diff --git a/CHANGELOG.md b/CHANGELOG.md index 5589940..7986a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,31 @@ `[BUGFIX]` +# 0.5.1 + +> This is the first release of OpenFunction CLI + +`[CHANGE]` + +`[FEATURE]` + +`[ENHANCEMENT]` + +`[BUGFIX]` +- Fix the issue that ofn install cannot install the latest version of OpenFunction #19 + +# 0.5.2 + +> This is the first release of OpenFunction CLI + +`[CHANGE]` + +`[FEATURE]` + +`[ENHANCEMENT]` +- Change the default version of OpenFunction to the latest stable version +- Add the `--force` option to force the operation +- Use the spinner instead of the original process display +- Adjust the function hierarchy to make some functions more generic + +`[BUGFIX]` diff --git a/README.md b/README.md index fdd7c3e..79b5b35 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,117 @@ # ![OpenFunctionCtl](docs/images/logo.png) -`cli` is the Command-Line Interface for [OpenFunction](https://github.com/OpenFunction/OpenFunction) +`cli` is the command-line interface for [OpenFunction](https://github.com/OpenFunction/OpenFunction). -The `cli` repo is used to track issues for the `OpenFunction`. this tool allows users to focus on the core functionality of the OpenFunction, while also presenting the relationship between the OpenFunction and its dependent components in a more abstract and friendly way to the user. +The `cli` repo is used to track issues for the `OpenFunction`. This tool allows you to focus on the core functionality of the OpenFunction, while also presents the relationship between the OpenFunction and its dependent components in a more abstract and friendly way. ## Main commands The main commands supported by the CLI are: - init: provides management for openfunction’s framework. -- install: install OpenFunction and its dependencies. -- uninstall: uninstall OpenFunction and its dependencies. -- create: create a function from a file or stdin. -- apply: apply a function from a file or stdin. +- install: installs OpenFunction and its dependencies. +- uninstall: uninstalls OpenFunction and its dependencies. +- create: creates a function from a file or stdin. +- apply: applies a function from a file or stdin. - get: prints a table of the most important information about the specified function. - get builder: prints important information about the builder. - - get serving:prints important information about the serving. -- delete: delete a specified the function. + - get serving: prints important information about the serving. +- delete: deletes the specified function. ## Getting started -Visit [ofn release](https://github.com/OpenFunction/cli/releases/) to select the `ofn` cli to deploy to your cluster. +Visit [ofn releases page](https://github.com/OpenFunction/cli/releases/) to download the `ofn` cli to deploy to your cluster. ### Use ofn to deploy OpenFunction -> We assume that you have placed the artifacts from the above step under the appropriate path in `PATH` and renamed it `ofn`. +> Make sure you put the artifacts from the above step under the appropriate path in `PATH` and rename it `ofn`. -You can use the `ofn install --all` to complete a simple deployment. By default, this command will install the *v0.4.0* version of OpenFunction for you, while it will skip the installation process for components that already exist (you can use the `--upgrade` command to overwrite these components). +Run `ofn install --all` to implement a simple deployment. By default, this command will install the *v0.4.0* version of OpenFunction and skips the installation of components that already exist. To overwrite the existing components, use the `--upgrade` command. -Please refer to the [ofn install docs](docs/install.md). +For more information, refer to the [ofn install document](docs/install.md). ```shell -# ofn install --all --upgrade +# ofn install --all Start installing OpenFunction and its dependencies. -Here are the components and corresponding versions to be installed: +The following components will be installed: +------------------+---------+ | COMPONENT | VERSION | +------------------+---------+ +| Knative Serving | 1.0.1 | +| Tekton Pipelines | 0.30.0 | +| OpenFunction | 0.5.0 | | Kourier | 1.0.1 | +| DefaultDomain | 1.0.1 | | Keda | 2.4.0 | -| Tekton Pipelines | 0.30.0 | -| OpenFunction | 0.4.0 | +| CertManager | 1.5.4 | | Dapr | 1.5.1 | -| CertManager | 1.1.0 | | Shipwright | 0.6.1 | -| Knative Serving | 1.0.1 | -| DefaultDomain | 1.0.1 | +| IngressNginx | 1.1.0 | +------------------+---------+ -You have used the `--upgrade` parameter, which means that the installation process will overwrite the components that already exist. -Make sure you know what happens when you do this. -Enter 'y' to continue and 'n' to abort: --> y -πŸ”„ -> INGRESS <- Installing Ingress... -πŸ”„ -> KNATIVE <- Installing Knative Serving... -πŸ”„ -> DAPR <- Installing Dapr... -πŸ”„ -> DAPR <- Downloading Dapr Cli binary... -πŸ”„ -> KEDA <- Installing Keda... -πŸ”„ -> CERTMANAGER <- Installing Cert Manager... -πŸ”„ -> SHIPWRIGHT <- Installing Shipwright... -πŸ”„ -> INGRESS <- Checking if Ingress is ready... -πŸ”„ -> KEDA <- Checking if Keda is ready... -πŸ”„ -> CERTMANAGER <- Checking if Cert Manager is ready... -πŸ”„ -> SHIPWRIGHT <- Checking if Shipwright is ready... -πŸ”„ -> KNATIVE <- Installing Kourier as Knative's gateway... -πŸ”„ -> KNATIVE <- Configuring Knative Serving's DNS... -πŸ”„ -> KNATIVE <- Checking if Knative Serving is ready... -βœ… -> CERTMANAGER <- Done! -πŸ”„ -> DAPR <- Initializing Dapr with Kubernetes mode... -βœ… -> SHIPWRIGHT <- Done! -βœ… -> KNATIVE <- Done! -βœ… -> INGRESS <- Done! -βœ… -> DAPR <- Done! -βœ… -> KEDA <- Done! -πŸ”„ -> OPENFUNCTION <- Installing OpenFunction... -πŸ”„ -> OPENFUNCTION <- Checking if OpenFunction is ready... -βœ… -> OPENFUNCTION <- Done! -πŸš€ Completed in 2m3.638035129s. + βœ“ Dapr - Completed! + βœ“ Keda - Completed! + βœ“ Knative Serving - Completed! + βœ“ Shipwright - Completed! + βœ“ Cert Manager - Completed! + βœ“ Ingress - Completed! + βœ“ OpenFunction - Completed! +πŸš€ Completed in 1m40.055438303s. + + β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— +β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ +β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ +β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β• β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ +β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ + β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•β• + +β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— +β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ +β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ +β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ +β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ +β•šβ•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β•β•β• ``` ### Use ofn to uninstall OpenFunction -> We assume that you have placed the artifacts from the above step under the appropriate path in `PATH` and renamed it `ofn`. +> Make sure you put the artifacts from the above step under the appropriate path in `PATH` and rename it `ofn`. -You can use `ofn uninstall --all` to easily uninstall OpenFunction and its dependencies (or just uninstall OpenFunction without arguments). +Run `ofn uninstall --all` to uninstall OpenFunction and its dependencies (or just uninstall OpenFunction without arguments). -Please refer to the [ofn uninstall docs](docs/uninstall.md). +For more information, refer to the [ofn uninstall document](docs/uninstall.md). ```shell -~# ofn uninstall --all +~# ofn uninstall --all -y Start uninstalling OpenFunction and its dependencies. The following components already exist: +------------------+---------+ | COMPONENT | VERSION | +------------------+---------+ -| Cert Manager | v1.5.4 | -| Ingress Nginx | 1.1.0 | -| Tekton Pipelines | v0.28.1 | -| Shipwright | 0.6.0 | -| OpenFunction | v0.4.0 | -| Dapr | 1.4.3 | +| OpenFunction | 0.5.0 | +| Knative Serving | 1.0.1 | +| Kourier | 1.0.1 | +| DefaultDomain | 1.0.1 | +| IngressNginx | 1.1.0 | | Keda | 2.4.0 | -| Knative Serving | 0.26.0 | +| Dapr | 1.5.1 | +| Shipwright | 0.6.1 | +| Tekton Pipelines | 0.30.0 | +| CertManager | 1.5.4 | +------------------+---------+ -You can see the list of components to be uninstalled and the list of components already exist in the cluster. -Make sure you know what happens when you do this. -Enter 'y' to continue and 'n' to abort: --> y -πŸ”„ -> OPENFUNCTION <- Uninstalling OpenFunction... -πŸ”„ -> KNATIVE <- Uninstalling Knative Serving... -πŸ”„ -> DAPR <- Uninstalling Dapr with Kubernetes mode... -πŸ”„ -> KEDA <- Uninstalling Keda... -πŸ”„ -> SHIPWRIGHT <- Uninstalling Tekton Pipeline & Shipwright... -πŸ”„ -> INGRESS <- Uninstalling Ingress... -πŸ”„ -> CERTMANAGER <- Uninstalling Cert Manager... -βœ… -> OPENFUNCTION <- Done! -βœ… -> DAPR <- Done! -πŸ”„ -> KNATIVE <- Uninstalling Kourier... -βœ… -> KEDA <- Done! -βœ… -> CERTMANAGER <- Done! -βœ… -> KNATIVE <- Done! -βœ… -> INGRESS <- Done! -βœ… -> SHIPWRIGHT <- Done! -πŸš€ Completed in 1m21.683329262s. + βœ“ Dapr - Completed! + βœ“ Keda - Completed! + βœ“ Knative Serving - Completed! + βœ“ Shipwright - Completed! + βœ“ Tekton Pipelines - Completed! + βœ“ Cert Manager - Completed! + βœ“ Ingress - Completed! + βœ“ OpenFunction - Completed! +πŸš€ Completed in 1m17.729501739s. ``` ### Build OpenFunction CLI -You can also use `make build` to build the OpenFunction CLI β€”β€” `ofn`. +To build the OpenFunction CLIβ€”β€”`ofn`, run `make build`. When the command is executed, you can find the artifact in the `. /dist` directory. -Move it to the appropriate path in the `PATH` so that you can use it in your environment. +Move it to an appropriate path in the `PATH` so that you can use it in your environment. ```shell ~# make build diff --git a/RELEASE.md b/RELEASE.md index eebdb83..bb1e6c9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,6 +7,8 @@ This page describes the release process and the currently planned schedule for u | release series | date (year-month-day) | release shepherd | |----------------|--------------------------------------------|---------------------------------------------| | v0.5.0 | 2021-12-21 | Laminar (GitHub: @tpiperatgod) | +| v0.5.1 | 2021-12-22 | Laminar (GitHub: @tpiperatgod) | +| v0.5.2 | 2022-01-03 | Laminar (GitHub: @tpiperatgod) | # How to cut a new release diff --git a/VERSION b/VERSION index 48080b4..7ac3424 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.5.0 \ No newline at end of file +v0.5.2 \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index 2d2f5ba..9bb0f45 100644 --- a/docs/install.md +++ b/docs/install.md @@ -17,6 +17,7 @@ This command will help you to install OpenFunction and its dependencies. --shipwright For installing ShipWright. --sync For installing OpenFunction Sync Runtime (To be supported). --upgrade Upgrade components to target version while installing. +--yes Automatic yes to prompts. ('-y' as a short form) --verbose Show verbose information. --version string Used to specify the version of OpenFunction to be installed. (default "v0.4.0") --timeout duration Set timeout time. Default is 5 minutes. (default 5m0s) @@ -52,11 +53,10 @@ ofn install --upgrade --all ### Install a specific version of OpenFunction -> default to v0.4.0 if no version specified +> default to the latest stable version The available versions are: -- v0.3.1 -- v0.4.0 +- any stable version - latest ```shell @@ -69,7 +69,7 @@ OpenFunction relies on several components like Knative Serving, Dapr, Keda, Ship The OpenFunction CLI provides a default compatibility matrix based on which OpenFunction CLI will install a default selected version of each component for each version of kubernetes. -The OpenFunction CLI keeps the installed component details in `$home/.ofn/inventory.yaml`. +The OpenFunction CLI keeps the installed component details in `$home/.ofn/-inventory.yaml`. | Components | Kubernetes 1.17 | Kubernetes 1.18 | Kubernetes 1.19 | Kubernetes 1.20+ | CLI Option | Description | | ---------------------- | --------------- | --------------- | --------------- | ---------------- | ---------------- | ---------------------------------------------- | diff --git a/docs/uninstall.md b/docs/uninstall.md index 97e1913..7c06f20 100644 --- a/docs/uninstall.md +++ b/docs/uninstall.md @@ -17,7 +17,8 @@ This command will help you to uninstall OpenFunction and its dependencies. --shipwright For uninstalling ShipWright. --sync For uninstalling OpenFunction Sync Runtime (To be supported). --verbose Show verbose information. ---version string Used to specify the version of OpenFunction to be uninstalled. (default "v0.4.0") +--yes Automatic yes to prompts. ('-y' as a short form) +--version string Used to specify the version of OpenFunction to be uninstalled. --wait Awaiting the results of the uninstallation. --timeout duration Set timeout time. Default is 5 minutes. (default 5m0s) ``` @@ -54,11 +55,10 @@ ofn uninstall --all --wait ### Uninstall a specific version of OpenFunction -> Default to v0.4.0 if no version specified +> Default to the version of the OpenFunction currently installed The available versions are: -- v0.3.1 -- v0.4.0 +- any stable version - latest ```shell @@ -67,7 +67,7 @@ ofn uninstall --version v0.4.0 ## Inventory -During installation, the OpenFunction CLI keeps the installed component details in `$home/.ofn/inventory.yaml`. So during the uninstallation, the OpenFunction CLI will remove the relevant components based on the contents of `$home/.ofn/inventory.yaml`. +During installation, the OpenFunction CLI keeps the installed component details in `$home/.ofn/-inventory.yaml`. So during the uninstallation, the OpenFunction CLI will remove the relevant components based on the contents of `$home/.ofn/-inventory.yaml`. In addition, the OpenFunction CLI supports obtaining the version of the component and the path to the component's yaml file from the environment variable. You can refer to the [Environment variables](install.md#environment-variables) for more information. diff --git a/go.mod b/go.mod index 6fe8bcc..b4831ca 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/OpenFunction/cli go 1.16 require ( + github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 github.com/fatih/color v1.7.0 github.com/jedib0t/go-pretty/v6 v6.2.4 + github.com/leaanthony/synx v0.1.0 + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 github.com/openfunction v0.3.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.2.1 diff --git a/go.sum b/go.sum index b5334ae..8c244b6 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/aerospike/aerospike-client-go v4.5.0+incompatible/go.mod h1:zj8LBEnWB github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b/go.mod h1:Tie46d3UWzXpj+Fh9+DQTyaUxEpFBPOLXrnx7nxlKRo= +github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 h1:vOVO0ypMfTt6tZacyI0kp+iCZb1XSNiYDqnzBWYgfe4= +github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs= github.com/ahmetb/gen-crd-api-reference-docs v0.3.1-0.20210420163308-c1402a70e2f1/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= @@ -852,6 +854,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubernetes-sigs/custom-metrics-apiserver v0.0.0-20210311094424-0ca2b1909cdc/go.mod h1:o4psv/D+LJC+NGyL66BoKWXLkzlJeUqhL6/3rjFKXw0= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= +github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE= github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod h1:UGmTpUd3rjbtfIpwAPrcfmGf/Z1HS95TATB+m57TPB8= @@ -998,6 +1002,8 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/pkg/cmd/subcommand/demo.go b/pkg/cmd/subcommand/demo.go index a846571..3bebb20 100644 --- a/pkg/cmd/subcommand/demo.go +++ b/pkg/cmd/subcommand/demo.go @@ -1,26 +1,26 @@ package subcommand import ( - "bufio" "context" "fmt" "os" + "os/signal" "runtime" - "strings" + "syscall" "time" "github.com/OpenFunction/cli/pkg/client" "github.com/OpenFunction/cli/pkg/cmd/util" + "github.com/OpenFunction/cli/pkg/cmd/util/spinners" "github.com/OpenFunction/cli/pkg/components/common" "github.com/OpenFunction/cli/pkg/components/inventory" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" "k8s.io/cli-runtime/pkg/genericclioptions" k8s "k8s.io/client-go/kubernetes" ) -// Demo is the commandline for 'init' sub command +// Demo is the commandline for 'demo' sub command type Demo struct { genericclioptions.IOStreams @@ -32,12 +32,10 @@ type Demo struct { Timeout time.Duration } -const DemoYamlFile = "https://raw.githubusercontent.com/OpenFunction/OpenFunction/main/config/samples/function-sample-serving-only.yaml" -const DemoYamlFileCN = "https://cdn.jsdelivr.net/gh/OpenFunction/OpenFunction@main/config/samples/function-sample-serving-only.yaml" - -func init() { - w = os.Stdout -} +const ( + DemoYamlFile = "https://raw.githubusercontent.com/OpenFunction/OpenFunction/main/config/samples/function-sample-serving-only.yaml" + DemoYamlFileCN = "https://cdn.jsdelivr.net/gh/OpenFunction/OpenFunction@main/config/samples/function-sample-serving-only.yaml" +) // NewDemo returns an initialized Init instance func NewDemo(ioStreams genericclioptions.IOStreams) *Demo { @@ -60,6 +58,7 @@ func NewCmdDemo(restClient util.Getter, ioStreams genericclioptions.IOStreams) * `, Example: "ofn demo", Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(i.ValidateArgs(cmd, args)) util.CheckErr(i.RunKind(cl, cmd)) }, } @@ -67,69 +66,64 @@ func NewCmdDemo(restClient util.Getter, ioStreams genericclioptions.IOStreams) * cmd.Flags().BoolVar(&i.Verbose, "verbose", false, "Show verbose information.") cmd.Flags().BoolVar(&i.RegionCN, "region-cn", false, "For users who have limited access to gcr.io or github.com.") cmd.Flags().BoolVar(&i.AutoPrune, "auto-prune", true, "Automatically clean up the demo environment.") - cmd.Flags().DurationVar(&i.Timeout, "timeout", 10*time.Minute, "Set timeout time. Default is 10 minutes.") + cmd.Flags().DurationVar(&i.Timeout, "timeout", 20*time.Minute, "Set timeout time. Default is 20 minutes.") return cmd } -func (i *Demo) RunKind(cl *k8s.Clientset, cmd *cobra.Command) error { - if i.AutoPrune { - defer i.DeleteKind() +func (i *Demo) ValidateArgs(cmd *cobra.Command, args []string) error { + v, e := getLatestStableVersion() + if e != nil { + return e } + i.OpenFunctionVersion = v + return nil +} - operator := common.NewOperator(runtime.GOOS, i.OpenFunctionVersion, i.Timeout, i.RegionCN, i.Verbose) - ti := util.NewTaskInformer("DEMO") - Continue := func() bool { - reader := bufio.NewReader(os.Stdin) - fmt.Fprintln(w, ti.BeforeTask("A Kind cluster will be created and the OpenFunction Demo will be launched in it...\n"+ - "Enter 'y' to continue and 'n' to abort:")) - - for { - fmt.Fprint(w, ti.BeforeTask("-> ")) - text, _ := reader.ReadString('\n') - // convert CRLF to LF - text = strings.Replace(text, "\n", "", -1) - - if strings.Compare("y", text) == 0 { - return true - } - if strings.Compare("n", text) == 0 { - return false - } +func (i *Demo) RunKind(cl *k8s.Clientset, cmd *cobra.Command) error { + defer func() { + if i.AutoPrune { + i.deleteCluster() } - } + }() + + operator := common.NewOperator(runtime.GOOS, i.OpenFunctionVersion, i.Timeout, i.RegionCN, i.Verbose) ctx, done := context.WithTimeout( context.Background(), i.Timeout, ) defer done() - inventoryPending := i.checkConditionsAndGetInventory() - fmt.Fprintln(w, ti.BeforeTask("Launching OpenFunction demo...\n"+ - "The following components will be installed for this demo:")) - printInventory(inventoryPending) - - if !Continue() { - return nil - } + util.BeforeTask(" -> The OpenFunction demonstration <-\n" + + "Start launching the cluster.") - grp1, g1ctx := errgroup.WithContext(ctx) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + done() + }() start := time.Now() - grp1.Go(func() error { - return i.InstallKind(g1ctx, operator) - }) + // grp1 for installing the cluster via Kind + grp1 := spinners.NewSpinnerGroup() + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Kind Cluster") + i.createCluster(ctx, spinner, operator) + }(ctx, 0) + grp1.Start(ctx) if err := grp1.Wait(); err != nil { - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } _, cl, err := client.NewKubeConfigClient() if err != nil { return err } - installinventoryPending, err := inventory.GetInventory( + inventoryPending, err := inventory.GetInventory( cl, i.RegionCN, true, @@ -143,106 +137,135 @@ func (i *Demo) RunKind(cl *k8s.Clientset, cmd *cobra.Command) error { if err != nil { return errors.Wrap(err, "failed to get pending inventory") } - operator.Inventory = installinventoryPending - - fmt.Fprintln(w, ti.BeforeTask("Start installing OpenFunction and its dependencies.\n"+ - "Here are the components and corresponding versions to be installed:")) - printInventory(inventory.GetVersionMap(installinventoryPending)) - - grp2, g2ctx := errgroup.WithContext(ctx) - grp2.Go(func() error { - return i.InstallDapr(g2ctx, operator) - }) - grp2.Go(func() error { - return i.installKeda(g2ctx, cl, operator) - }) - grp2.Go(func() error { - return i.installKnativeServing(g2ctx, cl, operator) - }) - grp2.Go(func() error { - return i.installShipwright(g2ctx, cl, operator) - }) - grp2.Go(func() error { - return i.installCertManager(g2ctx, cl, operator) - }) - + operator.Inventory = inventoryPending + + util.BeforeTask("Start installing OpenFunction and its dependencies.\n" + + "Here are the components and corresponding versions to be installed:") + printInventory(inventory.GetVersionMap(inventoryPending)) + + // Record the list of components + // that currently exist in the cluster. + if _, err := operator.GetInventoryRecord(ctx, false); err != nil { + return errors.Wrap(err, "failed to get inventory record") + } + defer operator.RecordInventory(ctx) + + // grp2 for installing dependent components + grp2 := spinners.NewSpinnerGroup() + count := 0 + + count += 1 + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("Dapr") + installDapr(ctx, spinner, operator) + }(ctx, count-1) + + count += 1 + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("Keda") + installKeda(ctx, spinner, cl, operator) + }(ctx, count-1) + + count += 1 + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("Knative Serving") + installKnativeServing(ctx, spinner, cl, operator) + }(ctx, count-1) + + count += 1 + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("Shipwright") + installShipwright(ctx, spinner, cl, operator) + }(ctx, count-1) + + count += 1 + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("Cert Manager") + installCertManager(ctx, spinner, cl, operator) + }(ctx, count-1) + + grp2.Start(ctx) if err := grp2.Wait(); err != nil { - util.CheckErr(i.DeleteKind()) - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } - grp3, g3ctx := errgroup.WithContext(ctx) + // grp3 for installing the latest stable OpenFunction + grp3 := spinners.NewSpinnerGroup() + + grp3.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp3.At(idx).WithName("OpenFunction") + installOpenFunction(ctx, spinner, cl, operator) + }(ctx, 0) - grp3.Go(func() error { - return i.installOpenFunction(g3ctx, cl, operator) - }) + grp3.Start(ctx) if err := grp3.Wait(); err != nil { - i.DeleteKind() - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } - grp4, g4ctx := errgroup.WithContext(ctx) + // grp4 for provisioning the demo + grp4 := spinners.NewSpinnerGroup() - grp4.Go(func() error { - if err := i.RunOpenFunctionDemo(g4ctx, cl, operator); err != nil { - return err - } - return nil - }) + grp4.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp4.At(idx).WithName("Demo") + i.provisionDemoFunction(ctx, spinner, cl, operator) + }(ctx, 0) + grp4.Start(ctx) if err := grp4.Wait(); err != nil { - util.CheckErr(i.DeleteKind()) - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } - end := time.Since(start) - fmt.Fprintln(w, ti.AllDone(end)) - return nil -} - -func (i *Demo) checkConditionsAndGetInventory() map[string]string { - i.OpenFunctionVersion = "v0.4.0" - inventory := map[string]string{ - "OpenFunction": i.OpenFunctionVersion, + if err := i.accessDemoFunction(ctx, cl, operator); err != nil { + return errors.New(util.TaskFail(err.Error())) } - return inventory + end := time.Since(start) + util.AllDone(end) + return nil } -func (i *Demo) InstallKind(ctx context.Context, operator *common.Operator) error { +func (i *Demo) createCluster(ctx context.Context, spinner *spinners.Spinner, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("KIND") - fmt.Fprintln(w, ti.TaskInfo("Installing Kind...")) - fmt.Fprintln(w, ti.TaskInfo("Downloading Kind binary...")) + spinner.Update("Downloading Kind binary...") if err := operator.DownloadKind(ctx); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to download Kind")) + spinner.Error(errors.Wrap(err, "Failed to download Kind")) + return } - fmt.Fprintln(w, ti.TaskInfo("Creating cluster...")) + + spinner.Update("Creating cluster...") if err := operator.CreateKindCluster(ctx); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to create kind cluster ")) + spinner.Error(errors.Wrap(err, "Failed to create kind cluster")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + + spinner.Done() + return } -func (i *Demo) DeleteKind() error { +func (i *Demo) deleteCluster() { operator := common.NewOperator(runtime.GOOS, i.OpenFunctionVersion, i.Timeout, i.RegionCN, i.Verbose) ctx, done := context.WithTimeout( context.Background(), i.Timeout, ) defer done() - ti := util.NewTaskInformer("DeleteKIND") + if err := operator.DeleteKind(ctx); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to delete Kind")) + util.TaskFail(err.Error()) + return } - - return nil } -func (i *Demo) RunOpenFunctionDemo(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func (i *Demo) provisionDemoFunction(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() var demo string @@ -252,234 +275,47 @@ func (i *Demo) RunOpenFunctionDemo(ctx context.Context, cl *k8s.Clientset, opera demo = DemoYamlFile } - ti := util.NewTaskInformer("DEMO") - - fmt.Fprintln(w, ti.TaskInfo("Run OpenFunctionDemo...")) + spinner.Update("Provisioning OpenFunction demo...") if err := operator.RunOpenFunction(ctx, demo); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to run OpenFunction demo")) + spinner.Error(errors.Wrap(err, "Failed to provision OpenFunction demo")) + return } NodeIP, err := operator.GetNodeIP(ctx) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to Get the node IP")) + spinner.Error(errors.Wrap(err, "Failed to get the Node IP address")) + return } if err := operator.PatchExternalIP(ctx, cl, NodeIP); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to Patch External IP")) + spinner.Error(errors.Wrap(err, "Failed to patch the External IP address")) + return } if err := operator.PatchMagicDNS(ctx, cl, NodeIP); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to Patch Magic DNS")) - } - EndPoint, err := operator.PrintEndpoint(ctx) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to Print the Endpoint")) - } - ti.TipsOnOpenfunctionDemo(EndPoint) - if err := operator.CurlOpenFunction(ctx, EndPoint); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to Curl OpenFunction")) - } - fmt.Fprintln(w, ti.TaskSuccess()) - - return nil -} - -func (i *Demo) InstallDapr(ctx context.Context, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - - ti := util.NewTaskInformer("DAPR") - - v := operator.Inventory[inventory.DaprName].GetVersion() - - fmt.Fprintln(w, ti.TaskInfo("Installing Dapr...")) - fmt.Fprintln(w, ti.TaskInfo("Downloading Dapr Cli binary...")) - if err := operator.DownloadDaprClient(ctx, v); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to download Dapr client")) + spinner.Error(errors.Wrap(err, "Failed to patch the Magic DNS")) + return } - fmt.Fprintln(w, ti.TaskInfo("Initializing Dapr with Kubernetes mode...")) - if err := operator.InitDapr(ctx, v); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Dapr")) - } - - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Demo) installKeda(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - - ti := util.NewTaskInformer("KEDA") - - fmt.Fprintln(w, ti.TaskInfo("Installing Keda...")) - - v := operator.Inventory[inventory.KedaName].GetVersion() - yamls, err := operator.Inventory[inventory.KedaName].GetYamlFile(v) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - - if err := operator.InstallKeda(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Keda")) - } - - fmt.Fprintln(w, ti.TaskInfo("Checking if Keda is ready...")) - if err := operator.CheckKedaIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Keda readiness")) - } - - fmt.Fprintln(w, ti.TaskSuccess()) - return nil - -} - -func (i *Demo) installKnativeServing(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - - ti := util.NewTaskInformer("KNATIVE") - - fmt.Fprintln(w, ti.TaskInfo("Installing Knative Serving...")) - - knv := operator.Inventory[inventory.KnativeServingName].GetVersion() - knYamls, err := operator.Inventory[inventory.KnativeServingName].GetYamlFile(knv) +func (i *Demo) accessDemoFunction(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { + fmt.Print(util.YellowItalic(" -> Fetching the URL of demo function...\r")) + endpoint, err := operator.PrintEndpoint(ctx) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + return errors.Wrap(err, "Failed to fetch the Endpoint") } - - if err := operator.InstallKnativeServing(ctx, knYamls["CRD"], knYamls["CORE"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Knative Serving")) - } - - fmt.Fprintln(w, ti.TaskInfo("Checking if Knative Serving is ready...")) - if err := operator.CheckKnativeServingIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Knative Serving readiness")) - } - - fmt.Fprintln(w, ti.TaskInfo("Configuring Knative Serving's DNS...")) - - ddv := operator.Inventory[inventory.ServingDefaultDomainName].GetVersion() - ddYamls, err := operator.Inventory[inventory.ServingDefaultDomainName].GetYamlFile(ddv) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - if err := operator.ConfigKnativeServingDefaultDomain(ctx, ddYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to config Knative Serving's DNS")) - } - - fmt.Fprintln(w, ti.TaskInfo("Installing Kourier as Knative's gateway...")) - - krv := operator.Inventory[inventory.KourierName].GetVersion() - krYamls, err := operator.Inventory[inventory.KourierName].GetYamlFile(krv) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - if err := operator.InstallKourier(ctx, cl, krYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Kourier")) - } - - fmt.Fprintln(w, ti.TaskInfo("Checking if Kourier is ready...")) - if err := operator.CheckKourierIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Kourier readiness")) - } - - fmt.Fprintln(w, ti.TaskSuccess()) - return nil - -} - -func (i *Demo) installShipwright(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - ti := util.NewTaskInformer("SHIPWRIGHT") - - fmt.Fprintln(w, ti.TaskInfo("Installing Tekton Pipelines...")) - - tkv := operator.Inventory[inventory.TektonPipelinesName].GetVersion() - tkYamls, err := operator.Inventory[inventory.TektonPipelinesName].GetYamlFile(tkv) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - - if err := operator.InstallTektonPipelines(ctx, tkYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Tekton Pipelines")) - } - - // Record the version of TektonPipelines - - fmt.Fprintln(w, ti.TaskInfo("Checking if Tekton Pipelines is ready...")) - if err := operator.CheckTektonPipelinesIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Tekton Pipelines readiness")) - } - - fmt.Fprintln(w, ti.TaskInfo("Installing Shipwright...")) - - swv := operator.Inventory[inventory.ShipwrightName].GetVersion() - swYamls, err := operator.Inventory[inventory.ShipwrightName].GetYamlFile(swv) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - - if err := operator.InstallShipwright(ctx, swYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Shipwright")) - } - - fmt.Fprintln(w, ti.TaskInfo("Checking if Shipwright is ready...")) - if err := operator.CheckShipwrightIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Shipwright readiness")) - } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil -} - -func (i *Demo) installCertManager(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - - ti := util.NewTaskInformer("CERTMANAGER") - - fmt.Fprintln(w, ti.TaskInfo("Installing Cert Manager...")) - - v := operator.Inventory[inventory.CertManagerName].GetVersion() - yamls, err := operator.Inventory[inventory.CertManagerName].GetYamlFile(v) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - - if err := operator.InstallCertManager(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Cert Manager")) - } - - fmt.Fprintln(w, ti.TaskInfo("Checking if Cert Manager is ready...")) - if err := operator.CheckCertManagerIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Cert Manager readiness")) - } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil -} - -func (i *Demo) installOpenFunction(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { - ctx, done := context.WithCancel(ctx) - defer done() - - ti := util.NewTaskInformer("OPENFUNCTION") - v := operator.Inventory[inventory.OpenFunctionName].GetVersion() - yamls, err := operator.Inventory[inventory.OpenFunctionName].GetYamlFile(v) - if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) - } - - fmt.Fprintln(w, ti.TaskInfo("Installing OpenFunction...")) - if err := operator.InstallOpenFunction(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install OpenFunction")) + fmt.Println(util.YellowItalic(" -> You can use the following URL to access the demo function:")) + fmt.Println(endpoint) + fmt.Print(util.YellowItalic("\n -> We are now accessing the URL above...\r")) + if rt, err := operator.CurlOpenFunction(ctx, endpoint); err != nil { + return errors.Wrap(err, "Failed to access the URL of OpenFunction demo") + } else { + fmt.Println(util.YellowItalic(" -> We have accessed the URL above and get the following information:")) + fmt.Println(rt) } - fmt.Fprintln(w, ti.TaskInfo("Checking if OpenFunction is ready...")) - if err := operator.CheckOpenFunctionIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check OpenFunction readiness")) - } - fmt.Fprintln(w, ti.TaskSuccess()) return nil } diff --git a/pkg/cmd/subcommand/install.go b/pkg/cmd/subcommand/install.go index 316c102..2cce45e 100644 --- a/pkg/cmd/subcommand/install.go +++ b/pkg/cmd/subcommand/install.go @@ -3,8 +3,10 @@ package subcommand import ( "bufio" "context" + "encoding/json" "fmt" - "io" + "io/ioutil" + "net/http" "os" "os/signal" "reflect" @@ -13,24 +15,24 @@ import ( "syscall" "time" - "k8s.io/apimachinery/pkg/util/version" - "github.com/OpenFunction/cli/pkg/client" "github.com/OpenFunction/cli/pkg/cmd/util" + "github.com/OpenFunction/cli/pkg/cmd/util/spinners" "github.com/OpenFunction/cli/pkg/components/common" "github.com/OpenFunction/cli/pkg/components/inventory" + "github.com/oliveagle/jsonpath" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/util/version" "k8s.io/cli-runtime/pkg/genericclioptions" k8s "k8s.io/client-go/kubernetes" ) -var ( - w io.Writer +const ( + openFunctionLatestReleaseUrl = "https://api.github.com/repos/OpenFunction/OpenFunction/releases/latest" ) -// Install is the commandline for 'init' sub command +// Install is the commandline for 'install' sub command type Install struct { genericclioptions.IOStreams @@ -47,15 +49,12 @@ type Install struct { RegionCN bool OpenFunctionVersion string DryRun bool - WithUpgrade bool + Upgrade bool + Yes bool Timeout time.Duration openFunctionVersion *version.Version } -func init() { - w = os.Stdout -} - // NewInstall returns an initialized Init instance func NewInstall(ioStreams genericclioptions.IOStreams) *Install { return &Install{ @@ -83,7 +82,7 @@ ofn install --async # For users who have limited access to gcr.io or github.com to install OpenFunction ofn install --region-cn --all -# Install a specific version of OpenFunction (default is v0.4.0) +# Install a specific version of OpenFunction ofn install --all --version v0.4.0 # See more at: https://github.com/OpenFunction/cli/blob/main/docs/install.md @@ -113,9 +112,10 @@ ofn install --all --version v0.4.0 cmd.Flags().BoolVar(&i.WithAll, "all", false, "For installing all dependencies.") cmd.Flags().BoolVar(&i.RegionCN, "region-cn", false, "For users who have limited access to gcr.io or github.com.") cmd.Flags().BoolVar(&i.DryRun, "dry-run", false, "Used to prompt for the components and their versions to be installed by the current command.") - cmd.Flags().BoolVar(&i.WithUpgrade, "upgrade", false, "Upgrade components to target version while installing.") - cmd.Flags().StringVar(&i.OpenFunctionVersion, "version", "v0.4.0", "Used to specify the version of OpenFunction to be installed.") - cmd.Flags().DurationVar(&i.Timeout, "timeout", 5*time.Minute, "Set timeout time. Default is 5 minutes.") + cmd.Flags().BoolVar(&i.Upgrade, "upgrade", false, "Upgrade components to target version while installing.") + cmd.Flags().BoolVarP(&i.Yes, "yes", "y", false, "Automatic yes to prompts.") + cmd.Flags().StringVar(&i.OpenFunctionVersion, "version", "", "Used to specify the version of OpenFunction to be installed.") + cmd.Flags().DurationVar(&i.Timeout, "timeout", 10*time.Minute, "Set timeout time. Default is 10 minutes.") // In order to avoid too many options causing misunderstandings among users, // we have hidden the following parameters, // but you can still find their usage instructions in the documentation. @@ -128,25 +128,31 @@ ofn install --all --version v0.4.0 } func (i *Install) ValidateArgs(cmd *cobra.Command, args []string) error { - ti := util.NewTaskInformer("") - if i.OpenFunctionVersion == common.LatestVersion { return nil } + if i.OpenFunctionVersion == "" { + v, e := getLatestStableVersion() + if e != nil { + return e + } + i.OpenFunctionVersion = v + } + v, err := version.ParseGeneric(i.OpenFunctionVersion) if err != nil { - return errors.New(ti.TaskFail(fmt.Sprintf( + return errors.New(util.TaskFail(fmt.Sprintf( "the specified version %s is not a valid version", i.OpenFunctionVersion, ))) } if valid, err := common.IsVersionValid(v); err != nil { - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } else { if !valid { - return errors.New(ti.TaskFail(fmt.Sprintf( + return errors.New(util.TaskFail(fmt.Sprintf( "the specified version %s is lower than the supported version %s", i.OpenFunctionVersion, common.BaseVersion, @@ -161,17 +167,16 @@ func (i *Install) ValidateArgs(cmd *cobra.Command, args []string) error { func (i *Install) RunInstall(cl *k8s.Clientset, cmd *cobra.Command) error { operator := common.NewOperator(runtime.GOOS, i.OpenFunctionVersion, i.Timeout, i.RegionCN, i.Verbose) - ti := util.NewTaskInformer("") continueFunc := func() bool { reader := bufio.NewReader(os.Stdin) - fmt.Fprintln(w, ti.BeforeTask("You have used the `--upgrade` parameter, which means that the installation process "+ - "will overwrite the components that already exist.\n"+ - "Please ensure that you understand the meaning of this command "+ - "and follow the prompts below to confirm the action.\n"+ - "Enter 'y' to continue and 'n' to abort:")) + util.BeforeTask("You have specified the `--upgrade` flag, which means that the installation process " + + "will upgrade components currently installed.\n" + + "Please make sure that you're aware of the consequences of this command " + + "and follow the prompts below to confirm the upgrade.\n" + + "Enter 'y' to continue and 'n' to abort:") for { - fmt.Fprint(w, ti.BeforeTask("-> ")) + fmt.Print(util.YellowItalic("-> ")) text, _ := reader.ReadString('\n') // convert CRLF to LF text = strings.Replace(text, "\n", "", -1) @@ -211,14 +216,18 @@ func (i *Install) RunInstall(cl *k8s.Clientset, cmd *cobra.Command) error { operator.Inventory = inventoryPending inventoryExist := getExistComponentsInventory(ctx, cl) - fmt.Fprintln(w, ti.BeforeTask("Start installing OpenFunction and its dependencies.\n"+ - "Here are the components and corresponding versions to be installed:")) + util.BeforeTask("Start installing OpenFunction and its dependencies.\n" + + "The following components will be installed:") printInventory(inventory.GetVersionMap(inventoryPending)) - if !reflect.DeepEqual(inventoryExist, map[string]bool{}) { - fmt.Fprintln(w, ti.BeforeTask("The following components already exist:")) + if !reflect.DeepEqual(inventoryExist, map[string]bool{}) && !i.Yes { + if !i.Upgrade { + util.BeforeTask("The following existing components will be skipped:") + } else { + util.BeforeTask("The following existing components will be upgraded:") + } for i, exist := range inventoryExist { if exist { - fmt.Fprintln(w, ti.BeforeTask(fmt.Sprintf("\t- %s", i))) + util.BeforeTask(fmt.Sprintf("\t- %s", i)) } } } @@ -227,7 +236,7 @@ func (i *Install) RunInstall(cl *k8s.Clientset, cmd *cobra.Command) error { return nil } - if i.WithUpgrade { + if i.Upgrade && !i.Yes { if !continueFunc() { return nil } @@ -247,87 +256,101 @@ func (i *Install) RunInstall(cl *k8s.Clientset, cmd *cobra.Command) error { done() }() - grp, gctx := errgroup.WithContext(ctx) - start := time.Now() + grp1 := spinners.NewSpinnerGroup() + count := 0 + if i.WithDapr { // If Dapr already exists and --upgrade is not specified, skip this step. - if !inventoryExist[inventory.DaprName] || i.WithUpgrade { - grp.Go(func() error { - return i.installDapr(gctx, operator) - }) - } else { - fmt.Fprintln(w, ti.SkipTask(inventory.DaprName)) + if !inventoryExist[inventory.DaprName] || i.Upgrade { + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Dapr") + installDapr(ctx, spinner, operator) + }(ctx, count-1) } } if i.WithKeda { // If Keda already exists and --upgrade is not specified, skip this step. - if !inventoryExist[inventory.KedaName] || i.WithUpgrade { - grp.Go(func() error { - return i.installKeda(gctx, cl, operator) - }) - } else { - fmt.Fprintln(w, ti.SkipTask("Keda")) + if !inventoryExist[inventory.KedaName] || i.Upgrade { + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Keda") + installKeda(ctx, spinner, cl, operator) + }(ctx, count-1) } } if i.WithKnative { // If Knative Serving already exists and --upgrade is not specified, skip this step. - if !inventoryExist[inventory.KnativeServingName] || i.WithUpgrade { - grp.Go(func() error { - return i.installKnativeServing(gctx, cl, operator) - }) - } else { - fmt.Fprintln(w, ti.SkipTask(inventory.KnativeServingName)) + if !inventoryExist[inventory.KnativeServingName] || i.Upgrade { + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Knative Serving") + installKnativeServing(ctx, spinner, cl, operator) + }(ctx, count-1) } } if i.WithShipWright { - grp.Go(func() error { - return i.installShipwright(gctx, cl, operator) - }) + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Shipwright") + installShipwright(ctx, spinner, cl, operator) + }(ctx, count-1) } if i.WithCertManager { // If Cert Manager already exists and --upgrade is not specified, skip this step. - if !inventoryExist[inventory.CertManagerName] || i.WithUpgrade { - grp.Go(func() error { - return i.installCertManager(gctx, cl, operator) - }) - } else { - fmt.Fprintln(w, ti.SkipTask(inventory.CertManagerName)) + if !inventoryExist[inventory.CertManagerName] || i.Upgrade { + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Cert Manager") + installCertManager(ctx, spinner, cl, operator) + }(ctx, count-1) } } if i.WithIngress { // If Ingress Nginx already exists and --upgrade is not specified, skip this step. - if !inventoryExist[inventory.IngressName] || i.WithUpgrade { - grp.Go(func() error { - return i.installIngress(gctx, cl, operator) - }) - } else { - fmt.Fprintln(w, ti.SkipTask(inventory.IngressName)) + if !inventoryExist[inventory.IngressName] || i.Upgrade { + count += 1 + grp1.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp1.At(idx).WithName("Ingress") + installIngress(ctx, spinner, cl, operator) + }(ctx, count-1) } } - if err := grp.Wait(); err != nil { - return errors.New(ti.TaskFail(err.Error())) + grp1.Start(ctx) + if err := grp1.Wait(); err != nil { + return errors.New(util.TaskFail(err.Error())) } - if err := i.installOpenFunction(ctx, cl, operator); err != nil { - return errors.New(ti.TaskFail(err.Error())) + grp2 := spinners.NewSpinnerGroup() + grp2.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := grp2.At(idx).WithName("OpenFunction") + installOpenFunction(ctx, spinner, cl, operator) + }(ctx, 0) + + grp2.Start(ctx) + if err := grp2.Wait(); err != nil { + return errors.New(util.TaskFail(err.Error())) } end := time.Since(start) - fmt.Fprintln(w, ti.AllDone(end)) - - if i.WithKnative { - ti.TipsOnUsingKnative() - } + util.AllDone(end) - ti.PrintOpenFunction() + util.PrintOpenFunction() return nil } @@ -411,258 +434,287 @@ func getExistComponentsInventory(ctx context.Context, cl *k8s.Clientset) map[str } func printInventory(inventory map[string]string) { - ti := util.NewTaskInformer("DRYRUN") - ti.PrintTable(inventory) + util.PrintInventory(inventory) } -func (i *Install) installDapr(ctx context.Context, operator *common.Operator) error { +func installDapr(ctx context.Context, spinner *spinners.Spinner, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("DAPR") - v := operator.Inventory[inventory.DaprName].GetVersion() - fmt.Fprintln(w, ti.TaskInfo("Installing Dapr...")) - fmt.Fprintln(w, ti.TaskInfo("Downloading Dapr Cli binary...")) + spinner.Update("Downloading Dapr CLI...") if err := operator.DownloadDaprClient(ctx, v); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to download Dapr client")) + spinner.Error(errors.Wrap(err, "Failed to download Dapr CLI")) + return } - fmt.Fprintln(w, ti.TaskInfo("Initializing Dapr with Kubernetes mode...")) + + spinner.Update("Initializing Dapr with Kubernetes mode...") if err := operator.InitDapr(ctx, v); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Dapr")) + spinner.Error(errors.Wrap(err, "Failed to init Dapr")) + return } // Record the version of Dapr operator.Records.Dapr = v - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Install) installKeda(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installKeda(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("KEDA") - - fmt.Fprintln(w, ti.TaskInfo("Installing Keda...")) + spinner.Update("Installing...") v := operator.Inventory[inventory.KedaName].GetVersion() yamls, err := operator.Inventory[inventory.KedaName].GetYamlFile(v) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallKeda(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Keda")) + spinner.Error(errors.Wrap(err, "Failed to install Keda")) + return } // Record the version of Keda operator.Records.Keda = v - fmt.Fprintln(w, ti.TaskInfo("Checking if Keda is ready...")) + spinner.Update("Checking if Keda is ready...") if err := operator.CheckKedaIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Keda readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Keda readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Install) installKnativeServing(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installKnativeServing(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("KNATIVE") - - fmt.Fprintln(w, ti.TaskInfo("Installing Knative Serving...")) + spinner.Update("Installing Knative Serving...") knv := operator.Inventory[inventory.KnativeServingName].GetVersion() knYamls, err := operator.Inventory[inventory.KnativeServingName].GetYamlFile(knv) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallKnativeServing(ctx, knYamls["CRD"], knYamls["CORE"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Knative Serving")) + spinner.Error(errors.Wrap(err, "Failed to install Knative Serving")) + return } // Record the version of KnativeServing operator.Records.KnativeServing = knv - fmt.Fprintln(w, ti.TaskInfo("Checking if Knative Serving is ready...")) + spinner.Update("Checking if Knative Serving is ready...") if err := operator.CheckKnativeServingIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Knative Serving readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Knative Serving readiness")) + return } - fmt.Fprintln(w, ti.TaskInfo("Configuring Knative Serving's DNS...")) - + spinner.Update("Configuring Knative Serving's DNS...") ddv := operator.Inventory[inventory.ServingDefaultDomainName].GetVersion() ddYamls, err := operator.Inventory[inventory.ServingDefaultDomainName].GetYamlFile(ddv) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.ConfigKnativeServingDefaultDomain(ctx, ddYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to config Knative Serving's DNS")) + spinner.Error(errors.Wrap(err, "Failed to config Knative Serving's DNS")) + return } // Record the version of DefaultDomain operator.Records.DefaultDomain = ddv - fmt.Fprintln(w, ti.TaskInfo("Installing Kourier as Knative's gateway...")) - + spinner.Update("Installing Kourier as Knative's gateway...") krv := operator.Inventory[inventory.KourierName].GetVersion() krYamls, err := operator.Inventory[inventory.KourierName].GetYamlFile(krv) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallKourier(ctx, cl, krYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Kourier")) + spinner.Error(errors.Wrap(err, "Failed to install Kourier")) + return } // Record the version of Kourier operator.Records.Kourier = krv - fmt.Fprintln(w, ti.TaskInfo("Checking if Kourier is ready...")) + spinner.Update("Checking if Kourier is ready...") if err := operator.CheckKourierIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Kourier readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Kourier readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Install) installShipwright(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installShipwright(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("SHIPWRIGHT") - - fmt.Fprintln(w, ti.TaskInfo("Installing Tekton Pipelines...")) - + spinner.Update("Installing Tekton Pipelines...") tkv := operator.Inventory[inventory.TektonPipelinesName].GetVersion() tkYamls, err := operator.Inventory[inventory.TektonPipelinesName].GetYamlFile(tkv) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallTektonPipelines(ctx, tkYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Tekton Pipelines")) + spinner.Error(errors.Wrap(err, "Failed to install Tekton Pipelines")) + return } // Record the version of TektonPipelines operator.Records.TektonPipelines = tkv - fmt.Fprintln(w, ti.TaskInfo("Checking if Tekton Pipelines is ready...")) + spinner.Update("Checking if Tekton Pipelines is ready...") if err := operator.CheckTektonPipelinesIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Tekton Pipelines readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Tekton Pipelines readiness")) + return } - fmt.Fprintln(w, ti.TaskInfo("Installing Shipwright...")) - + spinner.Update("Installing Shipwright...") swv := operator.Inventory[inventory.ShipwrightName].GetVersion() swYamls, err := operator.Inventory[inventory.ShipwrightName].GetYamlFile(swv) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallShipwright(ctx, swYamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Shipwright")) + spinner.Error(errors.Wrap(err, "Failed to install Shipwright")) + return } // Record the version of Shipwright operator.Records.Shipwright = swv - fmt.Fprintln(w, ti.TaskInfo("Checking if Shipwright is ready...")) + spinner.Update("Checking if Shipwright is ready...") if err := operator.CheckShipwrightIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Shipwright readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Shipwright readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + + spinner.Done() + return } -func (i *Install) installCertManager(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installCertManager(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("CERTMANAGER") - - fmt.Fprintln(w, ti.TaskInfo("Installing Cert Manager...")) - + spinner.Update("Installing...") v := operator.Inventory[inventory.CertManagerName].GetVersion() yamls, err := operator.Inventory[inventory.CertManagerName].GetYamlFile(v) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallCertManager(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Cert Manager")) + spinner.Error(errors.Wrap(err, "Failed to install Cert Manager")) + return } // Record the version of CertManager operator.Records.CertManager = v - fmt.Fprintln(w, ti.TaskInfo("Checking if Cert Manager is ready...")) + spinner.Update("Checking if Cert Manager is ready...") if err := operator.CheckCertManagerIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Cert Manager readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Cert Manager readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + + spinner.Done() + return } -func (i *Install) installIngress(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installIngress(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("INGRESS") - - fmt.Fprintln(w, ti.TaskInfo("Installing Ingress...")) - + spinner.Update("Installing...") v := operator.Inventory[inventory.IngressName].GetVersion() yamls, err := operator.Inventory[inventory.IngressName].GetYamlFile(v) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } if err := operator.InstallIngressNginx(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install Ingress")) + spinner.Error(errors.Wrap(err, "Failed to install Ingress")) + return } // Record the version of Ingress operator.Records.Ingress = v - fmt.Fprintln(w, ti.TaskInfo("Checking if Ingress is ready...")) + spinner.Update("Checking if Ingress is ready...") if err := operator.CheckIngressNginxIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check Ingress Nginx readiness")) + spinner.Error(errors.Wrap(err, "Failed to check Ingress Nginx readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + + spinner.Done() + return } -func (i *Install) installOpenFunction(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func installOpenFunction(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("OPENFUNCTION") - + spinner.Update("Installing...") v := operator.Inventory[inventory.OpenFunctionName].GetVersion() yamls, err := operator.Inventory[inventory.OpenFunctionName].GetYamlFile(v) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - fmt.Fprintln(w, ti.TaskInfo("Installing OpenFunction...")) if err := operator.InstallOpenFunction(ctx, yamls["MAIN"]); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to install OpenFunction")) + spinner.Error(errors.Wrap(err, "Failed to install OpenFunction")) + return } // Record the version of OpenFunction operator.Records.OpenFunction = v - fmt.Fprintln(w, ti.TaskInfo("Checking if OpenFunction is ready...")) + spinner.Update("Checking if OpenFunction is ready..") if err := operator.CheckOpenFunctionIsReady(ctx, cl); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to check OpenFunction readiness")) + spinner.Error(errors.Wrap(err, "Failed to check OpenFunction readiness")) + return } - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + + spinner.Done() + return +} + +func getLatestStableVersion() (string, error) { + var jsonData interface{} + + resp, err := http.Get(openFunctionLatestReleaseUrl) + if err != nil { + return "", errors.Wrap(err, "failed to fetch OpenFunction latest release") + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + json.Unmarshal(body, &jsonData) + res, err := jsonpath.JsonPathLookup(jsonData, "$.tag_name") + if err != nil { + return "", errors.Wrap(err, "failed to find the tag_name value in release information") + } + return res.(string), nil } diff --git a/pkg/cmd/subcommand/uninstall.go b/pkg/cmd/subcommand/uninstall.go index 5baffd5..fe7011f 100644 --- a/pkg/cmd/subcommand/uninstall.go +++ b/pkg/cmd/subcommand/uninstall.go @@ -13,17 +13,17 @@ import ( "github.com/OpenFunction/cli/pkg/client" "github.com/OpenFunction/cli/pkg/cmd/util" + "github.com/OpenFunction/cli/pkg/cmd/util/spinners" "github.com/OpenFunction/cli/pkg/components/common" "github.com/OpenFunction/cli/pkg/components/inventory" "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/util/version" "k8s.io/cli-runtime/pkg/genericclioptions" k8s "k8s.io/client-go/kubernetes" ) -// Uninstall is the commandline for 'init' sub command +// Uninstall is the commandline for 'uninstall' sub command type Uninstall struct { genericclioptions.IOStreams @@ -40,6 +40,7 @@ type Uninstall struct { RegionCN bool OpenFunctionVersion string DryRun bool + Yes bool WaitForCleared bool Timeout time.Duration } @@ -74,7 +75,7 @@ ofn uninstall --region-cn --all # Uninstall OpenFunction and wait for the uninstallation to complete (default timeout is 300s/5m) ofn uninstall --all --wait -# Uninstall a specific version of OpenFunction (default is v0.4.0) +# Uninstall a specific version of OpenFunction ofn uninstall --all --version v0.4.0 # See more at: https://github.com/OpenFunction/cli/blob/main/docs/uninstall.md @@ -105,8 +106,9 @@ ofn uninstall --all --version v0.4.0 cmd.Flags().BoolVar(&i.RegionCN, "region-cn", false, "For users who have limited access to gcr.io or github.com.") cmd.Flags().BoolVar(&i.DryRun, "dry-run", false, "Used to prompt for the components and their versions to be uninstalled by the current command.") cmd.Flags().BoolVar(&i.WaitForCleared, "wait", false, "Awaiting the results of the uninstallation.") - cmd.Flags().StringVar(&i.OpenFunctionVersion, "version", "v0.4.0", "Used to specify the version of OpenFunction to be uninstalled.") - cmd.Flags().DurationVar(&i.Timeout, "timeout", 5*time.Minute, "Set timeout time. Default is 5 minutes.") + cmd.Flags().BoolVarP(&i.Yes, "yes", "y", false, "Automatic yes to prompts.") + cmd.Flags().StringVar(&i.OpenFunctionVersion, "version", "", "Used to specify the version of OpenFunction to be uninstalled.") + cmd.Flags().DurationVar(&i.Timeout, "timeout", 10*time.Minute, "Set timeout time. Default is 10 minutes.") // In order to avoid too many options causing misunderstandings among users, // we have hidden the following parameters, // but you can still find their usage instructions in the documentation. @@ -119,25 +121,27 @@ ofn uninstall --all --version v0.4.0 } func (i *Uninstall) ValidateArgs(cmd *cobra.Command, args []string) error { - ti := util.NewTaskInformer("") - if i.OpenFunctionVersion == common.LatestVersion { return nil } + if i.OpenFunctionVersion == "" { + return nil + } + v, err := version.ParseGeneric(i.OpenFunctionVersion) if err != nil { - return errors.New(ti.TaskFail(fmt.Sprintf( + return errors.New(util.TaskFail(fmt.Sprintf( "the specified version %s is not a valid version", i.OpenFunctionVersion, ))) } if valid, err := common.IsVersionValid(v); err != nil { - return errors.New(ti.TaskFail(err.Error())) + return errors.New(util.TaskFail(err.Error())) } else { if !valid { - return errors.New(ti.TaskFail(fmt.Sprintf( + return errors.New(util.TaskFail(fmt.Sprintf( "the specified version %s is lower than the supported version %s", i.OpenFunctionVersion, common.BaseVersion, @@ -150,15 +154,14 @@ func (i *Uninstall) ValidateArgs(cmd *cobra.Command, args []string) error { func (i *Uninstall) RunUninstall(cl *k8s.Clientset, cmd *cobra.Command) error { operator := common.NewOperator(runtime.GOOS, i.OpenFunctionVersion, i.Timeout, i.RegionCN, i.Verbose) - ti := util.NewTaskInformer("") continueFunc := func() bool { reader := bufio.NewReader(os.Stdin) - fmt.Fprintln(w, ti.BeforeTask("Please ensure that you understand the meaning of this command "+ - "and follow the prompts below to confirm the action.\n"+ - "Enter 'y' to continue and 'n' to abort:")) + util.BeforeTask("Please ensure that you understand the meaning of this command " + + "and follow the prompts below to confirm the action.\n" + + "Enter 'y' to continue and 'n' to abort:") for { - fmt.Fprint(w, ti.BeforeTask("-> ")) + fmt.Print(util.YellowItalic("-> ")) text, _ := reader.ReadString('\n') // convert CRLF to LF text = strings.Replace(text, "\n", "", -1) @@ -201,14 +204,15 @@ func (i *Uninstall) RunUninstall(cl *k8s.Clientset, cmd *cobra.Command) error { } operator.Inventory = inventoryPending - fmt.Fprintln(w, ti.BeforeTask("Start uninstalling OpenFunction and its dependencies.")) - fmt.Fprintln(w, ti.BeforeTask("The following components already exist:")) + util.BeforeTask("Start uninstalling OpenFunction and its dependencies.") + util.BeforeTask("The following components already exist:") printInventory(inventoryExist) if i.DryRun { return nil } - if !continueFunc() { + + if !i.Yes && !continueFunc() { return nil } @@ -226,75 +230,101 @@ func (i *Uninstall) RunUninstall(cl *k8s.Clientset, cmd *cobra.Command) error { done() }() - grp, gctx := errgroup.WithContext(ctx) - start := time.Now() + group := spinners.NewSpinnerGroup() + count := 0 + if i.WithDapr { if operator.Records.Dapr != "" { - grp.Go(func() error { - return i.uninstallDapr(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Dapr") + uninstallDapr(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if i.WithKeda { if operator.Records.Keda != "" { - grp.Go(func() error { - return i.uninstallKeda(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Keda") + uninstallKeda(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if i.WithKnative { if operator.Records.KnativeServing != "" { - grp.Go(func() error { - return i.uninstallKnativeServing(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Knative Serving") + uninstallKnativeServing(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if i.WithShipWright { if operator.Records.Shipwright != "" { - grp.Go(func() error { - return i.uninstallShipwright(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Shipwright") + uninstallShipwright(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } if operator.Records.TektonPipelines != "" { - grp.Go(func() error { - return i.uninstallTektonPipelines(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Tekton Pipelines") + uninstallTektonPipelines(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if i.WithCertManager { if operator.Records.CertManager != "" { - grp.Go(func() error { - return i.uninstallCertManager(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Cert Manager") + uninstallCertManager(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if i.WithIngress { if operator.Records.Ingress != "" { - grp.Go(func() error { - return i.uninstallIngress(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("Ingress") + uninstallIngress(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } } if operator.Records.OpenFunction != "" { - grp.Go(func() error { - return i.uninstallOpenFunction(gctx, cl, operator) - }) + count += 1 + group.AddSpinner() + go func(ctx context.Context, idx int) { + spinner := group.At(idx).WithName("OpenFunction") + uninstallOpenFunction(ctx, spinner, cl, operator, i.WaitForCleared) + }(ctx, count-1) } - if err := grp.Wait(); err != nil { - return errors.New(ti.TaskFail(err.Error())) + group.Start(ctx) + if err := group.Wait(); err != nil { + return errors.New(util.TaskFail(err.Error())) } end := time.Since(start) - fmt.Fprintln(w, ti.AllDone(end)) + util.AllDone(end) return nil } @@ -320,64 +350,62 @@ func (i *Uninstall) mergeConditions() { //} } -func (i *Uninstall) uninstallDapr(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallDapr(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("DAPR") + spinner.Update("Uninstalling...") - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Dapr with Kubernetes mode...")) - if err := operator.UninstallDapr(ctx, cl, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Dapr")) + if err := operator.UninstallDapr(ctx, cl, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Dapr")) + return } // Reset version to null operator.Records.Dapr = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallKeda(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallKeda(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("KEDA") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Keda...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.KedaName].GetYamlFile(operator.Records.Keda) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KedaNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Keda")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KedaNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Keda")) + return } // Reset version to null operator.Records.Keda = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallKnativeServing(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallKnativeServing(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("KNATIVE") - if operator.Records.DefaultDomain != "" { - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Serving Default Domain...")) - + spinner.Update("Uninstalling Serving Default Domain...") yamls, err := operator.Inventory[inventory.ServingDefaultDomainName].GetYamlFile(operator.Records.DefaultDomain) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KnativeServingNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Serving Default Domain")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KnativeServingNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Serving Default Domain")) + return } // Reset version to null @@ -385,155 +413,152 @@ func (i *Uninstall) uninstallKnativeServing(ctx context.Context, cl *k8s.Clients } if operator.Records.Kourier != "" { - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Kourier...")) - + spinner.Update("Uninstalling Kourier...") yamls, err := operator.Inventory[inventory.KourierName].GetYamlFile(operator.Records.Kourier) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KedaNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Kourier")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.KedaNamespace, true, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Kourier")) + return } // Reset version to null operator.Records.Kourier = "" } - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Knative Serving...")) - + spinner.Update("Uninstalling Knative Serving...") yamls, err := operator.Inventory[inventory.KnativeServingName].GetYamlFile(operator.Records.KnativeServing) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.UninstallKnativeServing(ctx, cl, yamls["CRD"], yamls["CORE"], i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Knative Serving")) + if err := operator.UninstallKnativeServing(ctx, cl, yamls["CRD"], yamls["CORE"], waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Knative Serving")) + return } // Reset version to null operator.Records.KnativeServing = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallShipwright(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallShipwright(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("SHIPWRIGHT") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Shipwright...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.ShipwrightName].GetYamlFile(operator.Records.Shipwright) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.ShipwrightNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Shipwright")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.ShipwrightNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Shipwright")) + return } // Reset version to null operator.Records.Shipwright = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallTektonPipelines(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallTektonPipelines(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("TEKTON") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Tekton Pipeline...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.TektonPipelinesName].GetYamlFile(operator.Records.TektonPipelines) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.TektonPipelineNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Tekton Pipeline")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.TektonPipelineNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Tekton Pipeline")) + return } // Reset version to null operator.Records.TektonPipelines = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallCertManager(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallCertManager(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("CERTMANAGER") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Cert Manager...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.CertManagerName].GetYamlFile(operator.Records.CertManager) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.CertManagerNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Cert Manager")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.CertManagerNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Cert Manager")) + return } // Reset version to null operator.Records.CertManager = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallIngress(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallIngress(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("INGRESS") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling Ingress...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.IngressName].GetYamlFile(operator.Records.Ingress) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.IngressNginxNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall Ingress")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.IngressNginxNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall Ingress")) + return } // Reset version to null operator.Records.Ingress = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } -func (i *Uninstall) uninstallOpenFunction(ctx context.Context, cl *k8s.Clientset, operator *common.Operator) error { +func uninstallOpenFunction(ctx context.Context, spinner *spinners.Spinner, cl *k8s.Clientset, operator *common.Operator, waitForCleared bool) { ctx, done := context.WithCancel(ctx) defer done() - ti := util.NewTaskInformer("OPENFUNCTION") - - fmt.Fprintln(w, ti.TaskInfo("Uninstalling OpenFunction...")) - + spinner.Update("Uninstalling...") yamls, err := operator.Inventory[inventory.OpenFunctionName].GetYamlFile(operator.Records.OpenFunction) if err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to get yaml file")) + spinner.Error(errors.Wrap(err, "Failed to get yaml file")) + return } - if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.OpenFunctionNamespace, false, i.WaitForCleared); err != nil { - return errors.Wrap(err, ti.TaskFailWithTitle("Failed to uninstall OpenFunction")) + if err := operator.Uninstall(ctx, cl, yamls["MAIN"], common.OpenFunctionNamespace, false, waitForCleared); err != nil { + spinner.Error(errors.Wrap(err, "Failed to uninstall OpenFunction")) + return } // Reset version to null operator.Records.OpenFunction = "" - fmt.Fprintln(w, ti.TaskSuccess()) - return nil + spinner.Done() + return } diff --git a/pkg/cmd/util/print.go b/pkg/cmd/util/print.go index b25b1e1..e95dc58 100644 --- a/pkg/cmd/util/print.go +++ b/pkg/cmd/util/print.go @@ -15,6 +15,12 @@ import ( "k8s.io/cli-runtime/pkg/printers" ) +const ( + NormalMsg = "normal" + SuccessMsg = "success" + ErrorMsg = "error" +) + var ( Yellow = color.New(color.FgHiYellow, color.Bold).SprintFunc() YellowItalic = color.New(color.FgHiYellow, color.Bold, color.Italic).SprintFunc() @@ -145,49 +151,7 @@ func TranslateTimestampSince(timestamp metav1.Time) string { return duration.HumanDuration(time.Since(timestamp.Time)) } -// TaskInformer is a printer of task information. -type TaskInformer struct { - title string -} - -func NewTaskInformer(title string) *TaskInformer { - return &TaskInformer{ - title: title, - } -} - -func (ti *TaskInformer) BeforeTask(msg string) string { - return fmt.Sprintf("%s", YellowItalic(msg)) -} - -func (ti *TaskInformer) SkipTask(msg string) string { - str := fmt.Sprintf(" -> Skip the %s installation", msg) - return fmt.Sprintf("πŸš— %s", Green(str)) -} - -func (ti *TaskInformer) TaskInfo(msg string) string { - str := fmt.Sprintf(" -> %s <- %s", ti.title, msg) - return fmt.Sprintf("πŸ”„ %s", White(str)) -} - -func (ti *TaskInformer) TaskFail(msg string) string { - return fmt.Sprintf("❌ %s", Red(msg)) -} - -func (ti *TaskInformer) TaskFailWithTitle(msg string) string { - return fmt.Sprintf(" -> %s <- %s", ti.title, msg) -} - -func (ti *TaskInformer) TaskSuccess() string { - str := fmt.Sprintf(" -> %s <- Done!", ti.title) - return fmt.Sprintf("βœ… %s", White(str)) -} - -func (ti *TaskInformer) AllDone(t time.Duration) string { - return fmt.Sprintf("πŸš€ %s", WhiteBold(fmt.Sprintf("Completed in %s.", t))) -} - -func (ti *TaskInformer) PrintTable(inventory map[string]string) { +func PrintInventory(inventory map[string]string) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"Component", "Version"}) @@ -200,27 +164,19 @@ func (ti *TaskInformer) PrintTable(inventory map[string]string) { t.Render() } -func (ti *TaskInformer) TipsOnUsingKnative() { - fmt.Println(YellowItalic("Notice that you are using Knative runtime, " + - "you can refer to the following to configure Knative's network layer (Assuming you are using Kourier) and DNS. \n" + - "Where 1.2.3.4 can be replaced by your node address or loadbalancer address:")) - fmt.Println(YellowItalic("\n -> Configure the externalIPs for the Kourier service")) - fmt.Println("kubectl patch svc -n kourier-system kourier \\\n" + - " -p '{\"spec\": {\"type\": \"LoadBalancer\", \"externalIPs\": [\"1.2.3.4\"]}}'") - fmt.Println(YellowItalic("\n -> Configure the domain by using MagicDNS")) - fmt.Println("kubectl patch configmap/config-domain -n knative-serving \\\n" + - " --type merge --patch '{\"data\":{\"1.2.3.4.sslip.io\":\"\"}}'") - fmt.Println() +func BeforeTask(msg string) { + fmt.Println(YellowItalic(msg)) +} + +func TaskFail(msg string) string { + return fmt.Sprintf("%s Error reports: \n%s", Red(" ⇲"), msg) } -func (ti *TaskInformer) TipsOnOpenfunctionDemo(Endpoint string) { - fmt.Println(YellowItalic("Now we have configured the appropriate parameters for you, "+ - "You can use this address to access related functions : \n"), Endpoint) - fmt.Println() - fmt.Println(YellowItalic("We now use the curl command to access the address. The following information was returned:")) +func AllDone(t time.Duration) { + fmt.Println(fmt.Sprintf("πŸš€ %s", WhiteBold(fmt.Sprintf("Completed in %s.", t)))) } -func (ti *TaskInformer) PrintOpenFunction() { +func PrintOpenFunction() { fmt.Println(WhiteBold(` β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ diff --git a/pkg/cmd/util/spinners/spinner.go b/pkg/cmd/util/spinners/spinner.go new file mode 100644 index 0000000..958a1f5 --- /dev/null +++ b/pkg/cmd/util/spinners/spinner.go @@ -0,0 +1,108 @@ +package spinners + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/leaanthony/synx" +) + +// Status code constants. +const ( + runningStatus int = iota + successStatus + errorStatus + completed = "Completed!" + failed = "Failed!" + stopped = "Stopped!" +) + +var ( + green = color.New(color.Bold, color.FgGreen).Sprintf + red = color.New(color.Bold, color.FgRed).Sprintf + blue = color.New(color.Bold, color.FgBlue).Sprintf +) + +// Spinner defines a single s +type Spinner struct { + message *synx.String + status *synx.Int + group *SpinnerGroup + name *string +} + +func (s *Spinner) WithName(name string) *Spinner { + s.name = &name + return s +} + +func (s *Spinner) handleMessage(message string) string { + var msg string + if s.name != nil { + msg = fmt.Sprintf("%s - %s", *s.name, message) + } else { + msg = message + } + return msg +} + +// Update updates the spinner message +func (s *Spinner) Update(message string) { + s.message.SetValue(s.handleMessage(message)) +} + +// Done marks spinner as success +func (s *Spinner) Done() { + s.Update(completed) + s.stop(successStatus) +} + +// Error marks spinner as error +func (s *Spinner) Error(err error) { + s.ErrorWithMessage(failed, err) +} + +// ErrorWithMessage marks spinner as error and update message +func (s *Spinner) ErrorWithMessage(message string, err error) { + s.Update(message) + s.stop(errorStatus) + if err != nil { + s.group.err = err + s.group.errC <- err + } +} + +func (s *Spinner) stop(status int) { + s.status.SetValue(status) + s.group.redraw() + s.group.Done() +} + +func (s *Spinner) refresh() string { + switch s.status.GetValue() { + case successStatus: + return fmt.Sprintf("%s %s", green(s.getSymbol()), s.message.GetValue()) + case errorStatus: + return fmt.Sprintf("%s %s", red(s.getSymbol()), s.message.GetValue()) + default: + return fmt.Sprintf("%s %s", blue(s.getSymbol()), s.message.GetValue()) + } +} + +func (s *Spinner) getSymbol() string { + switch s.status.GetValue() { + case successStatus: + return s.group.successSymbol + case errorStatus: + return s.group.errorSymbol + default: + return s.group.currentFrame() + } +} + +func (s *Spinner) IsActive() bool { + if s.status.GetValue() == runningStatus { + return true + } + return false +} diff --git a/pkg/cmd/util/spinners/spinner_group.go b/pkg/cmd/util/spinners/spinner_group.go new file mode 100644 index 0000000..8c9e054 --- /dev/null +++ b/pkg/cmd/util/spinners/spinner_group.go @@ -0,0 +1,167 @@ +package spinners + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ahmetalpbalkan/go-cursor" + "github.com/leaanthony/synx" +) + +var ( + spinnerFrames = []string{ + "⠈⠁", + "β ˆβ ‘", + "⠈⠱", + "⠈⑱", + "β’€β‘±", + "β’„β‘±", + "β’„β‘±", + "β’†β‘±", + "⒎⑱", + "⒎⑰", + "⒎①", + "β’Žβ‘€", + "⒎⠁", + "⠎⠁", + "⠊⠁", + } +) + +// SpinnerGroup is a group of Spinners +type SpinnerGroup struct { + sync.Mutex + sync.WaitGroup + spinners []*Spinner + frames []string + currentFrameIdx int + successSymbol string + errorSymbol string + running bool + drawn bool + errC chan error + err error +} + +// At returns the Spinner at given 0-based index +func (g *SpinnerGroup) At(idx int) *Spinner { + return g.spinners[idx] +} + +// Start the spinners +func (g *SpinnerGroup) Start(ctx context.Context) { + g.Lock() + defer g.Unlock() + + if g.running { + return + } + g.running = true + + go func() { + ticker := time.NewTicker(100 * time.Millisecond) + for g.running { + select { + case <-ticker.C: + g.redraw() + case <-ctx.Done(): + g.stop(ctx.Err().Error()) + case <-g.errC: + g.stop(stopped) + } + } + }() +} + +func (g *SpinnerGroup) stop(errMsg string) { + for _, s := range g.spinners { + if s.IsActive() { + s.ErrorWithMessage(errMsg, nil) + } + } +} + +// Stop the spinners +func (g *SpinnerGroup) Stop() { + g.Lock() + defer g.Unlock() + g.running = false +} + +// Wait for all spinners to finish +func (g *SpinnerGroup) Wait() error { + g.WaitGroup.Wait() + g.Stop() + return g.err +} + +func (g *SpinnerGroup) redraw() { + g.Lock() + defer g.Unlock() + if !g.running { + return + } + if g.drawn { + fmt.Print(cursor.MoveUp(len(g.spinners))) + } + for _, spinner := range g.spinners { + fmt.Print(cursor.ClearEntireLine()) + fmt.Println(spinner.refresh()) + } + g.currentFrameIdx = (g.currentFrameIdx + 1) % len(g.frames) + g.drawn = true +} + +func (g *SpinnerGroup) currentFrame() string { + return g.frames[g.currentFrameIdx] +} + +func (g *SpinnerGroup) AddSpinner() *SpinnerGroup { + idx := len(g.spinners) + g.spinners = append(g.spinners, &Spinner{ + message: synx.NewString(fmt.Sprintf("Spinner #%d", idx+1)), + status: synx.NewInt(runningStatus), + group: g, + }) + g.Add(1) + return g +} + +// NewSpinnerGroupWithSize creates a SpinnerGroup with size +func NewSpinnerGroupWithSize(size int) *SpinnerGroup { + group := &SpinnerGroup{ + spinners: make([]*Spinner, size), + frames: spinnerFrames, + currentFrameIdx: 0, + successSymbol: " βœ“", + errorSymbol: " βœ—", + running: false, + drawn: false, + } + for i := 0; i < size; i++ { + group.spinners[i] = &Spinner{ + message: synx.NewString(fmt.Sprintf("Spinner #%d", i+1)), + status: synx.NewInt(runningStatus), + group: group, + } + } + group.Add(size) + return group +} + +// NewSpinnerGroup creates a SpinnerGroup +func NewSpinnerGroup() *SpinnerGroup { + group := &SpinnerGroup{ + spinners: []*Spinner{}, + frames: spinnerFrames, + currentFrameIdx: 0, + successSymbol: " βœ“", + errorSymbol: " βœ—", + running: false, + drawn: false, + errC: make(chan error, 1), + } + return group +} diff --git a/pkg/components/common/common.go b/pkg/components/common/common.go index b9ed3bd..4875cb6 100644 --- a/pkg/components/common/common.go +++ b/pkg/components/common/common.go @@ -295,7 +295,7 @@ func (o *Operator) UninstallKnativeServing( return err } cmd = fmt.Sprintf("delete -f %s", crdYamlFile) - if err := o.executor.KubectlExec(ctx, cmd, false); util.IgnoreNotFoundErr(err) != nil { + if err := o.executor.KubectlExec(ctx, cmd, true); util.IgnoreNotFoundErr(err) != nil { return err } @@ -416,15 +416,15 @@ func (o *Operator) PrintEndpoint(ctx context.Context) (string, error) { } } - EndPointCMD := "kubectl get ksvc -l openfunction.io/serving=$(kubectl get functions function-sample-serving-only -o jsonpath='{.status.serving.resourceRef}') -o jsonpath='{.items[0].status.url}'" - EndPoint, _, err := o.executor.Exec(EndPointCMD) + endpointCMD := "kubectl get ksvc -l openfunction.io/serving=$(kubectl get functions function-sample-serving-only -o jsonpath='{.status.serving.resourceRef}') -o jsonpath='{.items[0].status.url}'" + endpoint, _, err := o.executor.Exec(endpointCMD) if err != nil { return "", err } - return EndPoint, nil + return endpoint, nil } -func (o *Operator) CurlOpenFunction(ctx context.Context, endPoint string) error { +func (o *Operator) CurlOpenFunction(ctx context.Context, endPoint string) (string, error) { return o.executor.CurlOpenFunction(ctx, endPoint) } diff --git a/pkg/components/interface.go b/pkg/components/interface.go index 473b610..d0f6265 100644 --- a/pkg/components/interface.go +++ b/pkg/components/interface.go @@ -7,8 +7,8 @@ import ( ) const ( - OpenFunctionDir = ".ofn" - RecordFileName = "inventory.yaml" + OpenFunctionDir = ".ofn" + RecordFileNameTmpl = "%s-inventory.yaml" ) // OperatorExecutor is an executor abstraction @@ -22,5 +22,5 @@ type OperatorExecutor interface { GetInventoryRecord(ctx context.Context) (*inventory.Record, error) DownloadKind(ctx context.Context) error GetNodeIP(ctx context.Context) (string, error) - CurlOpenFunction(ctx context.Context, endPoint string) error + CurlOpenFunction(ctx context.Context, endPoint string) (string, error) } diff --git a/pkg/components/linux/linux.go b/pkg/components/linux/linux.go index 6391e32..f9df47e 100644 --- a/pkg/components/linux/linux.go +++ b/pkg/components/linux/linux.go @@ -81,6 +81,16 @@ func (e *Executor) KubectlExec( return nil } +func (e *Executor) getClusterName(ctx context.Context) (string, error) { + cmd := "kubectl config current-context" + + if out, _, err := e.Exec(cmd); err != nil { + return "", err + } else { + return strings.TrimSuffix(out, "\n"), nil + } +} + func (e *Executor) RecordInventory(ctx context.Context, inventoryMap map[string]string) error { dirname, err := os.UserHomeDir() if err != nil { @@ -93,7 +103,14 @@ func (e *Executor) RecordInventory(ctx context.Context, inventoryMap map[string] } } - filePath := filepath.Join(dirname, components.OpenFunctionDir, components.RecordFileName) + cName, err := e.getClusterName(ctx) + if err != nil { + return err + } + + fileName := fmt.Sprintf(components.RecordFileNameTmpl, cName) + + filePath := filepath.Join(dirname, components.OpenFunctionDir, fileName) file, err := os.Open(filePath) if err != nil { return err @@ -145,7 +162,14 @@ func (e *Executor) GetInventoryRecord(ctx context.Context) (*inventory.Record, e } } - filePath := filepath.Join(dirname, components.OpenFunctionDir, components.RecordFileName) + cName, err := e.getClusterName(ctx) + if err != nil { + return nil, err + } + + fileName := fmt.Sprintf(components.RecordFileNameTmpl, cName) + + filePath := filepath.Join(dirname, components.OpenFunctionDir, fileName) if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { file, err = os.Create(filePath) @@ -176,36 +200,35 @@ func (e *Executor) GetInventoryRecord(ctx context.Context) (*inventory.Record, e } func (e *Executor) DownloadKind(ctx context.Context) error { - DownloadCmd := "curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64" - if _, _, err := e.Exec(DownloadCmd); err != nil { + downloadCmd := "curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64" + if _, _, err := e.Exec(downloadCmd); err != nil { return err } - ChmodCmd := "chmod +x ./kind" - if _, _, err := e.Exec(ChmodCmd); err != nil { + chmodCmd := "chmod +x ./kind" + if _, _, err := e.Exec(chmodCmd); err != nil { return err } - MvCmd := "mv ./kind /usr/local/bin/kind" - if _, _, err := e.Exec(MvCmd); err != nil { + mvCmd := "mv ./kind /usr/local/bin/kind" + if _, _, err := e.Exec(mvCmd); err != nil { return err } return nil } func (e *Executor) GetNodeIP(ctx context.Context) (string, error) { - GetNodeIP := "docker exec openfunction-control-plane sh -c \"ip addr | grep eth0$ | grep -Eo 'inet ([0-9]*\\.){3}[0-9]*' | grep -Eo '([0-9]*\\.){3}[0-9]*' | tr -d '\\n'\"" - NodeIP, _, err := e.Exec(GetNodeIP) + getNodeIP := "docker exec openfunction-control-plane sh -c \"ip addr | grep eth0$ | grep -Eo 'inet ([0-9]*\\.){3}[0-9]*' | grep -Eo '([0-9]*\\.){3}[0-9]*' | tr -d '\\n'\"" + nodeIP, _, err := e.Exec(getNodeIP) if err != nil { return "", err } - return NodeIP, nil + return nodeIP, nil } -func (e *Executor) CurlOpenFunction(ctx context.Context, endPoint string) error { - CurlCMD := fmt.Sprintf("curl %s", endPoint) - res, _, err := e.Exec(CurlCMD) +func (e *Executor) CurlOpenFunction(ctx context.Context, endpoint string) (string, error) { + curlCMD := fmt.Sprintf("curl %s", endpoint) + res, _, err := e.Exec(curlCMD) if err != nil { - return err + return "", err } - fmt.Println(res) - return nil + return strings.TrimSuffix(res, "\n"), nil }