From aa0d512f60c92f2560f8a01ffffd9ba65ed5176e Mon Sep 17 00:00:00 2001 From: Fabien FLEUREAU Date: Tue, 3 Mar 2026 16:12:16 +0100 Subject: [PATCH 1/2] feat(container): add registry list command and public link on container create - Add `qovery container registry` subcommand with `list` subcommand supporting `--organization` and `--json` flags - Add `--json` flag to `qovery container create`; JSON output now includes `public_link` when a port is configured and a link is available - Use `encoding/json` for JSON output in container create (replaces hand-rolled fmt.Sprintf to avoid escaping issues) --- cmd/container_create.go | 120 +++++++++++++++++++++++++++++++++ cmd/container_registry.go | 25 +++++++ cmd/container_registry_list.go | 80 ++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 cmd/container_create.go create mode 100644 cmd/container_registry.go create mode 100644 cmd/container_registry_list.go diff --git a/cmd/container_create.go b/cmd/container_create.go new file mode 100644 index 00000000..9b9e0c91 --- /dev/null +++ b/cmd/container_create.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/qovery/qovery-cli/utils" + "github.com/qovery/qovery-client-go" + "github.com/spf13/cobra" +) + +var containerRegistryId string +var containerPort int32 +var containerCpu int32 +var containerMemory int32 +var containerMinRunningInstances int32 +var containerMaxRunningInstances int32 + +var containerCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a container service", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + tokenType, token, err := utils.GetAccessToken() + utils.CheckError(err) + + client := utils.GetQoveryClient(tokenType, token) + _, _, envId, err := getOrganizationProjectEnvironmentContextResourcesIds(client) + utils.CheckError(err) + + var ports []qovery.ServicePortRequestPortsInner + if containerPort > 0 { + portName := fmt.Sprintf("p%d", containerPort) + protocol := qovery.PORTPROTOCOLENUM_HTTP + ports = append(ports, qovery.ServicePortRequestPortsInner{ + Name: &portName, + InternalPort: containerPort, + ExternalPort: utils.Int32(443), + PubliclyAccessible: true, + IsDefault: utils.Bool(true), + Protocol: &protocol, + }) + } + + req := qovery.ContainerRequest{ + Name: containerName, + RegistryId: containerRegistryId, + ImageName: containerImageName, + Tag: containerTag, + Ports: ports, + Cpu: utils.Int32(containerCpu), + Memory: utils.Int32(containerMemory), + MinRunningInstances: utils.Int32(containerMinRunningInstances), + MaxRunningInstances: utils.Int32(containerMaxRunningInstances), + Healthchecks: *qovery.NewHealthcheck(), + } + + created, res, err := client.ContainersAPI.CreateContainer(context.Background(), envId).ContainerRequest(req).Execute() + if err != nil && res != nil && res.StatusCode != 201 { + result, _ := io.ReadAll(res.Body) + utils.PrintlnError(errors.Errorf("status code: %s ; body: %s", res.Status, string(result))) + } + utils.CheckError(err) + + var publicLink string + if len(ports) > 0 { + links, _, err := client.ContainerMainCallsAPI.ListContainerLinks(context.Background(), created.Id).Execute() + if err == nil { + for _, link := range links.GetResults() { + publicLink = link.Url + break + } + } + } + + if jsonFlag { + out := struct { + Id string `json:"id"` + Name string `json:"name"` + PublicLink string `json:"public_link,omitempty"` + }{Id: created.Id, Name: created.Name, PublicLink: publicLink} + j, _ := json.Marshal(out) + utils.Println(string(j)) + return + } + + msg := fmt.Sprintf("Container service %s created! (id: %s)", pterm.FgBlue.Sprintf("%s", created.Name), pterm.FgBlue.Sprintf("%s", created.Id)) + if publicLink != "" { + msg += fmt.Sprintf(" - Public link: %s", pterm.FgBlue.Sprintf("%s", publicLink)) + } + utils.Println(msg) + }, +} + +func init() { + containerCmd.AddCommand(containerCreateCmd) + containerCreateCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name") + containerCreateCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") + containerCreateCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") + containerCreateCmd.Flags().StringVarP(&containerName, "container", "n", "", "Container Name") + containerCreateCmd.Flags().StringVarP(&containerRegistryId, "registry", "", "", "Container Registry ID") + containerCreateCmd.Flags().StringVarP(&containerImageName, "image-name", "", "", "Container Image Name") + containerCreateCmd.Flags().StringVarP(&containerTag, "tag", "t", "", "Container Image Tag") + containerCreateCmd.Flags().Int32VarP(&containerPort, "port", "p", 0, "Container Port (0 = no port exposed)") + containerCreateCmd.Flags().Int32VarP(&containerCpu, "cpu", "", 500, "CPU in millicores (e.g. 500 = 0.5 vCPU)") + containerCreateCmd.Flags().Int32VarP(&containerMemory, "memory", "", 512, "Memory in MB") + containerCreateCmd.Flags().Int32VarP(&containerMinRunningInstances, "min-instances", "", 1, "Minimum number of running instances") + containerCreateCmd.Flags().Int32VarP(&containerMaxRunningInstances, "max-instances", "", 1, "Maximum number of running instances") + containerCreateCmd.Flags().BoolVarP(&jsonFlag, "json", "", false, "JSON output") + + _ = containerCreateCmd.MarkFlagRequired("container") + _ = containerCreateCmd.MarkFlagRequired("registry") + _ = containerCreateCmd.MarkFlagRequired("image-name") + _ = containerCreateCmd.MarkFlagRequired("tag") +} diff --git a/cmd/container_registry.go b/cmd/container_registry.go new file mode 100644 index 00000000..76ba752e --- /dev/null +++ b/cmd/container_registry.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "os" + + "github.com/qovery/qovery-cli/utils" + "github.com/spf13/cobra" +) + +var containerRegistryCmd = &cobra.Command{ + Use: "registry", + Short: "Manage container registries", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + if len(args) == 0 { + _ = cmd.Help() + os.Exit(0) + } + }, +} + +func init() { + containerCmd.AddCommand(containerRegistryCmd) +} diff --git a/cmd/container_registry_list.go b/cmd/container_registry_list.go new file mode 100644 index 00000000..4ad49b70 --- /dev/null +++ b/cmd/container_registry_list.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "context" + "encoding/json" + + "github.com/qovery/qovery-cli/pkg/usercontext" + "github.com/qovery/qovery-cli/utils" + "github.com/qovery/qovery-client-go" + "github.com/spf13/cobra" +) + +var containerRegistryListCmd = &cobra.Command{ + Use: "list", + Short: "List container registries", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + tokenType, token, err := utils.GetAccessToken() + utils.CheckError(err) + + client := utils.GetQoveryClient(tokenType, token) + organizationId, err := usercontext.GetOrganizationContextResourceId(client, organizationName) + utils.CheckError(err) + + registries, _, err := client.ContainerRegistriesAPI.ListContainerRegistry(context.Background(), organizationId).Execute() + utils.CheckError(err) + + if jsonFlag { + utils.Println(getContainerRegistryJsonOutput(registries.GetResults())) + return + } + + var data [][]string + for _, registry := range registries.GetResults() { + url := "" + if registry.Url != nil { + url = *registry.Url + } + kind := "" + if registry.Kind != nil { + kind = string(*registry.Kind) + } + data = append(data, []string{registry.Id, *registry.Name, kind, url}) + } + + utils.CheckError(utils.PrintTable([]string{"Id", "Name", "Kind", "URL"}, data)) + }, +} + +func getContainerRegistryJsonOutput(registries []qovery.ContainerRegistryResponse) string { + var results []interface{} + for _, registry := range registries { + url := "" + if registry.Url != nil { + url = *registry.Url + } + kind := "" + if registry.Kind != nil { + kind = string(*registry.Kind) + } + results = append(results, map[string]interface{}{ + "id": registry.Id, + "name": registry.Name, + "kind": kind, + "url": url, + }) + } + + j, err := json.Marshal(results) + utils.CheckError(err) + + return string(j) +} + +func init() { + containerRegistryCmd.AddCommand(containerRegistryListCmd) + containerRegistryListCmd.Flags().StringVarP(&organizationName, "organization", "", "", "Organization Name") + containerRegistryListCmd.Flags().BoolVarP(&jsonFlag, "json", "", false, "JSON output") +} From 1567e206ebdd5fc8792a302bec1d3220a65d921d Mon Sep 17 00:00:00 2001 From: Fabien FLEUREAU Date: Mon, 9 Mar 2026 07:16:34 +0100 Subject: [PATCH 2/2] fix(container): add nil guard for registry.Name and check marshal error - Guard registry.Name (a *string) before dereferencing in both the table loop and the JSON output function in container_registry_list.go - Check json.Marshal error in container_create.go instead of silently discarding it, consistent with the rest of the codebase --- cmd/container_create.go | 3 ++- cmd/container_registry_list.go | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/container_create.go b/cmd/container_create.go index 9b9e0c91..d6d6e2c4 100644 --- a/cmd/container_create.go +++ b/cmd/container_create.go @@ -84,7 +84,8 @@ var containerCreateCmd = &cobra.Command{ Name string `json:"name"` PublicLink string `json:"public_link,omitempty"` }{Id: created.Id, Name: created.Name, PublicLink: publicLink} - j, _ := json.Marshal(out) + j, err := json.Marshal(out) + utils.CheckError(err) utils.Println(string(j)) return } diff --git a/cmd/container_registry_list.go b/cmd/container_registry_list.go index 4ad49b70..e0d2c273 100644 --- a/cmd/container_registry_list.go +++ b/cmd/container_registry_list.go @@ -41,7 +41,11 @@ var containerRegistryListCmd = &cobra.Command{ if registry.Kind != nil { kind = string(*registry.Kind) } - data = append(data, []string{registry.Id, *registry.Name, kind, url}) + name := "" + if registry.Name != nil { + name = *registry.Name + } + data = append(data, []string{registry.Id, name, kind, url}) } utils.CheckError(utils.PrintTable([]string{"Id", "Name", "Kind", "URL"}, data)) @@ -59,9 +63,13 @@ func getContainerRegistryJsonOutput(registries []qovery.ContainerRegistryRespons if registry.Kind != nil { kind = string(*registry.Kind) } + name := "" + if registry.Name != nil { + name = *registry.Name + } results = append(results, map[string]interface{}{ "id": registry.Id, - "name": registry.Name, + "name": name, "kind": kind, "url": url, })