diff --git a/cmd/container_deploy.go b/cmd/container_deploy.go index a4bd2909..bb5fec9e 100644 --- a/cmd/container_deploy.go +++ b/cmd/container_deploy.go @@ -27,7 +27,7 @@ var containerDeployCmd = &cobra.Command{ } if containerName == "" && containerNames == "" { - utils.PrintlnError(fmt.Errorf("use either --cronjob \"\" or --cronjobs \", \" but not both at the same time")) + utils.PrintlnError(fmt.Errorf("use either --container \"\" or --containers \", \" but not both at the same time")) os.Exit(1) panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 } diff --git a/cmd/helm.go b/cmd/helm.go new file mode 100644 index 00000000..4ed6e79a --- /dev/null +++ b/cmd/helm.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "github.com/qovery/qovery-cli/utils" + "github.com/spf13/cobra" + "os" +) + +var helmName string +var helmNames string +var targetHelmName string + +var helmCmd = &cobra.Command{ + Use: "helm", + Short: "Manage helms", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + if len(args) == 0 { + _ = cmd.Help() + os.Exit(0) + } + }, +} + +func init() { + rootCmd.AddCommand(helmCmd) +} diff --git a/cmd/helm_clone.go b/cmd/helm_clone.go new file mode 100644 index 00000000..e167bfae --- /dev/null +++ b/cmd/helm_clone.go @@ -0,0 +1,116 @@ +package cmd + +import ( + "context" + "fmt" + "github.com/go-errors/errors" + "github.com/pterm/pterm" + "io" + "os" + + "github.com/qovery/qovery-cli/utils" + "github.com/qovery/qovery-client-go" + "github.com/spf13/cobra" +) + +var helmCloneCmd = &cobra.Command{ + Use: "clone", + Short: "Clone a helm", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + client := utils.GetQoveryClient(tokenType, token) + organizationId, projectId, envId, err := getOrganizationProjectEnvironmentContextResourcesIds(client) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + helm, err := getHelmContextResource(client, helmName, envId) + + if err != nil { + utils.PrintlnError(fmt.Errorf("helm %s not found", helmName)) + utils.PrintlnInfo("You can list all helms with: qovery helm list") + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + targetProjectId := projectId // use same project as the source project + if targetProjectName != "" { + + targetProjectId, err = getProjectContextResourceId(client, targetProjectName, organizationId) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + } + + targetEnvironmentId := envId // use same env as the source env + if targetEnvironmentName != "" { + + targetEnvironmentId, err = getEnvironmentContextResourceId(client, targetEnvironmentName, targetProjectId) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + } + + if targetHelmName == "" { + // use same helm name as the source helm + targetHelmName = helm.Name + } + + req := qovery.CloneServiceRequest{ + Name: targetHelmName, + EnvironmentId: targetEnvironmentId, + } + + clonedService, res, err := client.HelmsAPI.CloneHelm(context.Background(), helm.Id).CloneServiceRequest(req).Execute() + + if err != nil { + // print http body error message + if res.StatusCode != 200 { + result, _ := io.ReadAll(res.Body) + utils.PrintlnError(errors.Errorf("status code: %s ; body: %s", res.Status, string(result))) + } + + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + name := "" + if clonedService != nil { + name = clonedService.Name + } + + utils.Println(fmt.Sprintf("Helm %s cloned!", pterm.FgBlue.Sprintf(name))) + }, +} + + +func init() { + helmCmd.AddCommand(helmCloneCmd) + helmCloneCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name") + helmCloneCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") + helmCloneCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") + helmCloneCmd.Flags().StringVarP(&helmName, "helm", "n", "", "Helm Name") + helmCloneCmd.Flags().StringVarP(&targetProjectName, "target-project", "", "", "Target Project Name") + helmCloneCmd.Flags().StringVarP(&targetEnvironmentName, "target-environment", "", "", "Target Environment Name") + helmCloneCmd.Flags().StringVarP(&targetHelmName, "target-helm-name", "", "", "Target Helm Name") + + _ = helmCloneCmd.MarkFlagRequired("helm") +} diff --git a/cmd/helm_delete.go b/cmd/helm_delete.go new file mode 100644 index 00000000..e4bfd2cc --- /dev/null +++ b/cmd/helm_delete.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/qovery/qovery-cli/utils" +) + +var helmDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a helm", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + if helmName == "" && helmNames == "" { + utils.PrintlnError(fmt.Errorf("use either --helm \"\" or --helms \", \" but not both at the same time")) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + if helmName != "" && helmNames != "" { + utils.PrintlnError(fmt.Errorf("you can't use --helm and --helms at the same time")) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + client := utils.GetQoveryClient(tokenType, token) + _, _, envId, err := getOrganizationProjectEnvironmentContextResourcesIds(client) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + if helmNames != "" { + // wait until service is ready + for { + if utils.IsEnvironmentInATerminalState(envId, client) { + break + } + + utils.Println(fmt.Sprintf("Waiting for environment %s to be ready..", pterm.FgBlue.Sprintf(envId))) + time.Sleep(5 * time.Second) + } + + helms, _, err := client.HelmsAPI.ListHelms(context.Background(), envId).Execute() + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + var serviceIds []string + for _, helmName := range strings.Split(helmNames, ",") { + trimmedHelmName := strings.TrimSpace(helmName) + helm := utils.FindByHelmName(helms.GetResults(), trimmedHelmName) + if helm == nil { + utils.PrintlnError(fmt.Errorf("helm %s not found", trimmedHelmName)) + utils.PrintlnInfo("You can list all helms with: qovery helm list") + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + serviceIds = append(serviceIds, helm.Id) + } + + _, err = utils.DeleteServices(client, envId, serviceIds, utils.HelmType) + + if watchFlag { + utils.WatchEnvironment(envId, "unused", client) + } else { + utils.Println(fmt.Sprintf("Deleting helms %s in progress..", pterm.FgBlue.Sprintf(helmNames))) + } + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + return + } + + helms, _, err := client.HelmsAPI.ListHelms(context.Background(), envId).Execute() + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + helm := utils.FindByHelmName(helms.GetResults(), helmName) + + if helm == nil { + utils.PrintlnError(fmt.Errorf("helm %s not found", helmName)) + utils.PrintlnInfo("You can list all helms with: qovery helm list") + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + msg, err := utils.DeleteService(client, envId, helm.Id, utils.HelmType, watchFlag) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + if msg != "" { + utils.PrintlnInfo(msg) + return + } + + if watchFlag { + utils.Println(fmt.Sprintf("Helm %s deleted!", pterm.FgBlue.Sprintf(helmName))) + } else { + utils.Println(fmt.Sprintf("Deleting helm %s in progress..", pterm.FgBlue.Sprintf(helmName))) + } + }, +} + +func init() { + helmCmd.AddCommand(helmDeleteCmd) + helmDeleteCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name") + helmDeleteCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") + helmDeleteCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") + helmDeleteCmd.Flags().StringVarP(&helmName, "helm", "n", "", "Helm Name") + helmDeleteCmd.Flags().StringVarP(&helmNames, "helms", "", "", "Helm Names (comma separated) (ex: --helms \"helm1,helm2\")") + helmDeleteCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch helm status until it's ready or an error occurs") +} diff --git a/cmd/helm_list.go b/cmd/helm_list.go new file mode 100644 index 00000000..3b51d66b --- /dev/null +++ b/cmd/helm_list.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "context" + "github.com/qovery/qovery-cli/utils" + "github.com/spf13/cobra" + "os" +) + +var helmListCmd = &cobra.Command{ + Use: "list", + Short: "List helms", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + client := utils.GetQoveryClient(tokenType, token) + _, _, envId, err := getOrganizationProjectEnvironmentContextResourcesIds(client) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + helms, _, err := client.HelmsAPI.ListHelms(context.Background(), envId).Execute() + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + statuses, _, err := client.EnvironmentMainCallsAPI.GetEnvironmentStatuses(context.Background(), envId).Execute() + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + + var data [][]string + + for _, helm := range helms.GetResults() { + data = append(data, []string{helm.Id, helm.Name, "Helm", + utils.FindStatusTextWithColor(statuses.GetHelms(), helm.Id), helm.UpdatedAt.String()}) + } + + err = utils.PrintTable([]string{"Id", "Name", "Type", "Status", "Last Update"}, data) + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + }, +} + +func init() { + helmCmd.AddCommand(helmListCmd) + helmListCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name") + helmListCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") + helmListCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") +} diff --git a/cmd/service_list.go b/cmd/service_list.go index 027d6b82..45bbcad5 100644 --- a/cmd/service_list.go +++ b/cmd/service_list.go @@ -75,6 +75,14 @@ var serviceListCmd = &cobra.Command{ panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 } + helms, _, err := client.HelmsAPI.ListHelms(context.Background(), envId).Execute() + + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + statuses, _, err := client.EnvironmentMainCallsAPI.GetEnvironmentStatuses(context.Background(), envId).Execute() if err != nil { @@ -118,6 +126,10 @@ var serviceListCmd = &cobra.Command{ data = append(data, []string{database.Name, "Database", utils.FindStatusTextWithColor(statuses.GetDatabases(), database.Id)}) } + for _, helm := range helms.GetResults() { + data = append(data, []string{helm.Name, "Helm", utils.FindStatusTextWithColor(statuses.GetHelms(), helm.Id)}) + } + err = utils.PrintTable([]string{"Name", "Type", "Status"}, data) if err != nil { @@ -317,6 +329,29 @@ func getJobContextResource(qoveryAPIClient *qovery.APIClient, jobName string, en return job, nil } +func getHelmContextResource(qoveryAPIClient *qovery.APIClient, helmName string, environmentId string) (*qovery.HelmResponse, error) { + if strings.TrimSpace(environmentId) == "" { + // avoid making a call to the API if the environment id is not set + return nil, nil + } + + // find helms id by name + helms, _, err := qoveryAPIClient.HelmsAPI.ListHelms(context.Background(), environmentId).Execute() + + if err != nil { + return nil, err + } + + helm := utils.FindByHelmName(helms.GetResults(), helmName) + + if helm == nil { + return nil, errors.Errorf("helm %s not found", helmName) + } + + return helm, nil +} + + func getServiceJsonOutput(statuses qovery.EnvironmentStatuses, apps []qovery.Application, containers []qovery.ContainerResponse, jobs []qovery.JobResponse, databases []qovery.Database) string { var results []interface{} diff --git a/go.mod b/go.mod index a1bb821e..6ed7eae0 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a github.com/pterm/pterm v0.12.55 - github.com/qovery/qovery-client-go v0.0.0-20231208111827-88f2dd1feed0 + github.com/qovery/qovery-client-go v0.0.0-20231218142939-5f77c5a27bb7 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index b37f5f64..db006176 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,12 @@ github.com/qovery/qovery-client-go v0.0.0-20231026155011-32f75bc052b7 h1:ud76488 github.com/qovery/qovery-client-go v0.0.0-20231026155011-32f75bc052b7/go.mod h1:5QD7sC1Z6XCCYd31c4XKVwGdEOjvtgG0NDcaVDoWb+o= github.com/qovery/qovery-client-go v0.0.0-20231208111827-88f2dd1feed0 h1:HGsxRtKkHQiqk+PutyzcHKfICDMdNDnNc83FDk8DNY4= github.com/qovery/qovery-client-go v0.0.0-20231208111827-88f2dd1feed0/go.mod h1:5QD7sC1Z6XCCYd31c4XKVwGdEOjvtgG0NDcaVDoWb+o= +github.com/qovery/qovery-client-go v0.0.0-20231218094923-0f684434ba0c h1:C0D5dKhbbnblt4trlWpfbwMtW7j1kEt3gnYIQZbUIYw= +github.com/qovery/qovery-client-go v0.0.0-20231218094923-0f684434ba0c/go.mod h1:5QD7sC1Z6XCCYd31c4XKVwGdEOjvtgG0NDcaVDoWb+o= +github.com/qovery/qovery-client-go v0.0.0-20231218100840-79b5831d4fc2 h1:fO+LVI9c6o95eSN4IwbkUN2LGQwBnXPlyK9Pg0VhOSs= +github.com/qovery/qovery-client-go v0.0.0-20231218100840-79b5831d4fc2/go.mod h1:5QD7sC1Z6XCCYd31c4XKVwGdEOjvtgG0NDcaVDoWb+o= +github.com/qovery/qovery-client-go v0.0.0-20231218142939-5f77c5a27bb7 h1:FukfJyZOIuYCJgyqh96DpQshcEAzWP65WhhMCAeiIeg= +github.com/qovery/qovery-client-go v0.0.0-20231218142939-5f77c5a27bb7/go.mod h1:5QD7sC1Z6XCCYd31c4XKVwGdEOjvtgG0NDcaVDoWb+o= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/utils/qovery.go b/utils/qovery.go index f45103f9..77da5334 100644 --- a/utils/qovery.go +++ b/utils/qovery.go @@ -418,6 +418,7 @@ const ( ContainerType ServiceType = "container" DatabaseType ServiceType = "database" JobType ServiceType = "job" + HelmType ServiceType = "helm" ) type Service struct { @@ -1018,6 +1019,16 @@ func FindByDatabaseName(databases []qovery.Database, name string) *qovery.Databa return nil } +func FindByHelmName(helms []qovery.HelmResponse, name string) *qovery.HelmResponse { + for _, h := range helms { + if h.Name == name { + return &h + } + } + + return nil +} + func FindByCustomDomainName(customDomains []qovery.CustomDomain, name string) *qovery.CustomDomain { for _, d := range customDomains { if d.Domain == name { @@ -1671,6 +1682,21 @@ func DeleteService(client *qovery.APIClient, envId string, serviceId string, ser WatchJob(serviceId, envId, client) } + return "", nil + } + } + case HelmType: + for _, helm := range statuses.GetHelms() { + if helm.Id == serviceId && IsTerminalState(helm.State) { + _, err := client.HelmMainCallsAPI.DeleteHelm(context.Background(), serviceId).Execute() + if err != nil { + return "", err + } + + if watchFlag { + WatchJob(serviceId, envId, client) + } + return "", nil } } @@ -1774,6 +1800,25 @@ func DeleteServices(client *qovery.APIClient, envId string, serviceIds []string, return "", err } + return "", nil + } + case HelmType: + for _, helm := range statuses.GetHelms() { + if _, ok := serviceIdsSet[helm.Id]; ok && !IsTerminalState(helm.State) { + cannotDelete = true + } + } + if !cannotDelete { + _, err := client.EnvironmentActionsAPI. + DeleteSelectedServices(context.Background(), envId). + EnvironmentServiceIdsAllRequest(qovery.EnvironmentServiceIdsAllRequest{ + HelmIds: serviceIds, + }). + Execute() + if err != nil { + return "", err + } + return "", nil } } @@ -1938,6 +1983,21 @@ func RedeployService(client *qovery.APIClient, envId string, serviceId string, s WatchJob(serviceId, envId, client) } + return "", nil + } + } + case HelmType: + for _, helm := range statuses.GetHelms() { + if helm.Id == serviceId && IsTerminalState(helm.State) { + _, _, err := client.HelmActionsAPI.RedeployHelm(context.Background(), serviceId).Execute() + if err != nil { + return "", err + } + + if watchFlag { + WatchContainer(serviceId, envId, client) + } + return "", nil } }