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
9 changes: 5 additions & 4 deletions docs/stackit_server_service-account_attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ Attach a service account to a server
Attach a service account to a server

```
stackit server service-account attach SERVICE_ACCOUNT_EMAIL [flags]
stackit server service-account attach [flags]
```

### Examples

```
Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy"
$ stackit server service-account attach xxx@sa.stackit.cloud --server-id yyy
$ stackit server service-account attach --service-account-email xxx@sa.stackit.cloud --server-id yyy
```

### Options

```
-h, --help Help for "stackit server service-account attach"
-s, --server-id string Server ID
-h, --help Help for "stackit server service-account attach"
-s, --server-id string Server ID
-a, --service-account-email string Service Account Email
```

### Options inherited from parent commands
Expand Down
9 changes: 5 additions & 4 deletions docs/stackit_server_service-account_detach.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ Detach a service account from a server
Detach a service account from a server

```
stackit server service-account detach SERVICE_ACCOUNT_EMAIL [flags]
stackit server service-account detach [flags]
```

### Examples

```
Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy"
$ stackit server service-account detach xxx@sa.stackit.cloud --server-id yyy
$ stackit server service-account detach --service-account-email xxx@sa.stackit.cloud --server-id yyy
```

### Options

```
-h, --help Help for "stackit server service-account detach"
-s, --server-id string Server id
-h, --help Help for "stackit server service-account detach"
-s, --server-id string Server id
-a, --service-account-email string Service Account Email
```

### Options inherited from parent commands
Expand Down
28 changes: 22 additions & 6 deletions internal/cmd/server/service-account/attach/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package attach
import (
"context"
"fmt"
"strings"

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

Expand All @@ -20,9 +21,11 @@ import (
)

const (
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL"
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed 2026-12-03

serverIdFlag = "server-id"

serviceAccFlag = "service-account-email"
)

type inputModel struct {
Expand All @@ -33,14 +36,14 @@ type inputModel struct {

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("attach %s", serviceAccMailArg),
Use: "attach",
Short: "Attach a service account to a server",
Long: "Attach a service account to a server",
Args: args.SingleArg(serviceAccMailArg, nil),
Args: args.SingleOptionalArg(serviceAccMailArg, nil), // Deprecated: positional argument is not used anymore, use the flag instead, will be removed 2026-12-03
Example: examples.Build(
examples.NewExample(
`Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy"`,
"$ stackit server service-account attach xxx@sa.stackit.cloud --server-id yyy",
"$ stackit server service-account attach --service-account-email xxx@sa.stackit.cloud --server-id yyy",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -85,18 +88,31 @@ func NewCmd(params *types.CmdParams) *cobra.Command {

func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")

cmd.Flags().VarP(flags.EmailFlag(), serviceAccFlag, "a", "Service Account Email")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}

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

var serviceAccMail string
if cmd.Flags().Changed(serviceAccFlag) {
serviceAccMail = flags.FlagToStringValue(p, cmd, serviceAccFlag)
} else if len(inputArgs) > 0 {
serviceAccMail = inputArgs[0]
p.Warn("using a positional argument for the service account email is deprecated and will be removed on 2026-12-03. Please use '--%s' instead.\n", serviceAccFlag)
} else {
return nil, fmt.Errorf(`service account must be specified by using either the --%s flag or (deprecated) as a positional argument`, serviceAccFlag)
}

if serviceAccMail == "" || !strings.Contains(serviceAccMail, "@") {
return nil, fmt.Errorf("invalid service account email format: %q", serviceAccMail)
}

model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Expand Down
28 changes: 22 additions & 6 deletions internal/cmd/server/service-account/detach/detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package detach
import (
"context"
"fmt"
"strings"

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

Expand All @@ -20,9 +21,11 @@ import (
)

const (
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL"
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed 2026-12-03

serverIdFlag = "server-id"

serviceAccFlag = "service-account-email"
)

type inputModel struct {
Expand All @@ -33,14 +36,14 @@ type inputModel struct {

func NewCmd(params *types.CmdParams) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("detach %s", serviceAccMailArg),
Use: "detach",
Short: "Detach a service account from a server",
Long: "Detach a service account from a server",
Args: args.SingleArg(serviceAccMailArg, nil),
Args: args.SingleOptionalArg(serviceAccMailArg, nil), // Deprecated: positional argument is not used anymore, use the flag instead, will be removed 2026-12-03
Example: examples.Build(
examples.NewExample(
`Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy"`,
"$ stackit server service-account detach xxx@sa.stackit.cloud --server-id yyy",
"$ stackit server service-account detach --service-account-email xxx@sa.stackit.cloud --server-id yyy",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -85,18 +88,31 @@ func NewCmd(params *types.CmdParams) *cobra.Command {

func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server id")

cmd.Flags().VarP(flags.EmailFlag(), serviceAccFlag, "a", "Service Account Email")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}

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

var serviceAccMail string
if cmd.Flags().Changed(serviceAccFlag) {
serviceAccMail = flags.FlagToStringValue(p, cmd, serviceAccFlag)
} else if len(inputArgs) > 0 {
serviceAccMail = inputArgs[0]
p.Warn("using a positional argument for the service account email is deprecated and will be removed on 2026-12-03. Please use '--%s' instead.\n", serviceAccFlag)
} else {
return nil, fmt.Errorf(`service account must be specified by using either the --%s flag or (deprecated) as a positional argument`, serviceAccFlag)
}

if serviceAccMail == "" || !strings.Contains(serviceAccMail, "@") {
return nil, fmt.Errorf("invalid service account email format: %q", serviceAccMail)
}

model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Expand Down
37 changes: 37 additions & 0 deletions internal/pkg/flags/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package flags

import (
"fmt"
"strings"

"github.com/spf13/pflag"
)

type emailFlag struct {
value string
}

// Ensure the implementation satisfies the expected interface
var _ pflag.Value = &emailFlag{}

// EmailFlag returns a flag which must be a valid Email.
func EmailFlag() *emailFlag {
return &emailFlag{}
}

func (f *emailFlag) String() string {
return f.value
}

func (f *emailFlag) Set(value string) error {
isEmail := value != "" && strings.Contains(value, "@")
if !isEmail {
return fmt.Errorf("invalid email address: %s", value)
}
f.value = value
return nil
}

func (f *emailFlag) Type() string {
return "string"
}
61 changes: 61 additions & 0 deletions internal/pkg/flags/email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package flags

import (
"testing"

"github.com/spf13/cobra"
)

func TestEmailFlag(t *testing.T) {
tests := []struct {
description string
value string
isValid bool
}{
{
description: "valid",
value: "test@test",
isValid: true,
},
{
description: "empty",
value: "",
isValid: false,
},
{
description: "invalid",
value: "invalid-email",
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
flag := EmailFlag()
cmd := &cobra.Command{
Use: "test",
RunE: func(_ *cobra.Command, _ []string) error {
return nil
},
}
cmd.Flags().Var(flag, "test-flag", "test")

err := cmd.Flags().Set("test-flag", tt.value)

if !tt.isValid && err == nil {
t.Fatalf("did not fail on invalid input")
}
if !tt.isValid {
return
}

if err != nil {
t.Fatalf("failed on valid input: %v", err)
}
value := FlagToStringValue(nil, cmd, "test-flag")
if value != tt.value {
t.Fatalf("flag did not return set value")
}
})
}
}
Loading