Skip to content
1 change: 1 addition & 0 deletions docs/stackit_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ stackit server [flags]
* [stackit server reboot](./stackit_server_reboot.md) - Reboots a server
* [stackit server rescue](./stackit_server_rescue.md) - Rescues an existing server
* [stackit server resize](./stackit_server_resize.md) - Resizes the server to the given machine type
* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers
* [stackit server service-account](./stackit_server_service-account.md) - Allows attaching/detaching service accounts to servers
* [stackit server start](./stackit_server_start.md) - Starts an existing server or allocates the server if deallocated
* [stackit server stop](./stackit_server_stop.md) - Stops an existing server
Expand Down
35 changes: 35 additions & 0 deletions docs/stackit_server_security-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## stackit server security-group

Allows attaching/detaching security groups to servers

### Synopsis

Allows attaching/detaching security groups to servers.

```
stackit server security-group [flags]
```

### Options

```
-h, --help Help for "stackit server security-group"
```

### Options inherited from parent commands

```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```

### SEE ALSO

* [stackit server](./stackit_server.md) - Provides functionality for servers
* [stackit server security-group attach](./stackit_server_security-group_attach.md) - Attaches a security group to a server
* [stackit server security-group detach](./stackit_server_security-group_detach.md) - Detaches a security group from a server

42 changes: 42 additions & 0 deletions docs/stackit_server_security-group_attach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## stackit server security-group attach

Attaches a security group to a server

### Synopsis

Attaches a security group to a server.

```
stackit server security-group attach [flags]
```

### Examples

```
Attach a security group with ID "xxx" to a server with ID "yyy"
$ stackit server security-group attach --server-id yyy --security-group-id xxx
```

### Options

```
-h, --help Help for "stackit server security-group attach"
--security-group-id string Security Group ID
--server-id string Server ID
```

### Options inherited from parent commands

```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```

### SEE ALSO

* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers

42 changes: 42 additions & 0 deletions docs/stackit_server_security-group_detach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## stackit server security-group detach

Detaches a security group from a server

### Synopsis

Detaches a security group from a server.

```
stackit server security-group detach [flags]
```

### Examples

```
Detach a security group with ID "xxx" from a server with ID "yyy"
$ stackit server security-group detach --server-id yyy --security-group-id xxx
```

### Options

```
-h, --help Help for "stackit server security-group detach"
--security-group-id string Security Group ID
--server-id string Server ID
```

### Options inherited from parent commands

```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--region string Target region for region-specific requests
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```

### SEE ALSO

* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers

119 changes: 119 additions & 0 deletions internal/cmd/server/security-group/attach/attach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package attach

import (
"context"
"fmt"

"github.com/stackitcloud/stackit-cli/internal/pkg/types"

"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)

const (
serverIdFlag = "server-id"
securityGroupIdFlag = "security-group-id"
)

type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
SecurityGroupId string
}

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: "attach",
Short: "Attaches a security group to a server",
Long: "Attaches a security group to a server.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Attach a security group with ID "xxx" to a server with ID "yyy"`,
`$ stackit server security-group attach --server-id yyy --security-group-id xxx`,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(params.Printer, cmd, args)
if err != nil {
return err
}

// Configure API client
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
if err != nil {
return err
}

serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.Region, model.ServerId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get server name: %v", err)
serverLabel = model.ServerId
} else if serverLabel == "" {
serverLabel = model.ServerId
}

securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.Region, model.SecurityGroupId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err)
securityGroupLabel = model.SecurityGroupId
}

prompt := fmt.Sprintf("Are you sure you want to attach security group %q to server %q?", securityGroupLabel, serverLabel)
err = params.Printer.PromptForConfirmation(prompt)
if err != nil {
return err
}

// Call API
req := buildRequest(ctx, model, apiClient)
if err := req.Execute(); err != nil {
return fmt.Errorf("attach security group to server: %w", err)
}

params.Printer.Info("Attached security group %q to server %q\n", securityGroupLabel, serverLabel)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
params.Printer.Info("Attached security group %q to server %q\n", securityGroupLabel, serverLabel)
params.Printer.Outputf("Attached security group %q to server %q\n", securityGroupLabel, serverLabel)

This is often handled wrong in the codebase, that's why I'll explain it in detail. Please make sure to remember this in the future and especially look out for it in code reviews you will do in the future. 😅

Outputf prints to stdout, Info prints to stderr. See the code examples below for details.

Since this output here is a success message and no error message, it belongs into the stdout output from my perspective.

// Print an output using Printf to the defined output (falling back to Stderr if not set).
// If output format is set to none, it does nothing
func (p *Printer) Outputf(msg string, args ...any) {
outputFormat := viper.GetString(outputFormatKey)
if outputFormat == NoneOutputFormat {
return
}
p.Cmd.Printf(msg, args...)
}

// Print an Info level output to the defined Err output (falling back to Stderr if not set).
// If the verbosity level is not Debug or Info, it does nothing.
func (p *Printer) Info(msg string, args ...any) {
if !p.IsVerbosityDebug() && !p.IsVerbosityInfo() {
return
}
p.Cmd.PrintErrf(msg, args...)
}


return nil
},
}
configureFlags(cmd)
return cmd
}

func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID")
cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, "Security Group ID")

err := flags.MarkFlagsRequired(cmd, serverIdFlag, securityGroupIdFlag)
cobra.CheckErr(err)
}

func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}

model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag),
}

p.DebugInputModel(model)
return &model, nil
}

func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddSecurityGroupToServerRequest {
req := apiClient.AddSecurityGroupToServer(ctx, model.ProjectId, model.Region, model.ServerId, model.SecurityGroupId)
return req
}
Loading
Loading