Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions cmd/container_create.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The json.Marshal error is silently discarded with _ on this line (j, _ := json.Marshal(out)). Throughout the codebase, every other use of json.Marshal in JSON output paths (e.g., container_list.go:85, cluster_list.go:79, container_registry_list.go:88) assigns the error and handles it with PrintlnError + os.Exit(1). This is inconsistent with the established codebase convention and could silently produce an empty output on a marshalling failure. The error should be checked and handled.

Suggested change
j, _ := json.Marshal(out)
j, err := json.Marshal(out)
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}

Copilot uses AI. Check for mistakes.
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")
}
25 changes: 25 additions & 0 deletions cmd/container_registry.go
Original file line number Diff line number Diff line change
@@ -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)
}
80 changes: 80 additions & 0 deletions cmd/container_registry_list.go
Original file line number Diff line number Diff line change
@@ -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})
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registry.Name is a *string field, as confirmed by the client-go type (set via SetName in tests, using &containerRegistryRequest.Name in the mock). At line 57, it is dereferenced with *registry.Name without a nil check, while registry.Url (line 50) and registry.Kind (line 54) are both guarded with nil checks before dereferencing. If Name is nil for any registry, this will panic at runtime. A nil guard (similar to how Url is handled) or use of a getter method should be applied here.

Suggested change
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})

Copilot uses AI. Check for mistakes.
}

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,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In getContainerRegistryJsonOutput, registry.Name is stored directly into the map as a *string (line 82), unlike url and kind which are always resolved to plain string values (lines 72-79). When Name is nil, json.Marshal will output "name": null rather than "name": "", making the JSON output inconsistent with the other fields. The name field should be resolved to a plain string (possibly using a nil guard or a getter) just like url and kind are.

Copilot uses AI. Check for mistakes.
"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")
}
Loading