Skip to content
Open
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
23 changes: 17 additions & 6 deletions internal/cmd/server/service-account/attach/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,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 after 2026-12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12-31


serverIdFlag = "server-id"

serviceAccFlag = "service-account-email"
)

type inputModel struct {
Expand All @@ -33,14 +35,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),
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 +87,27 @@ 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 after 2026-12. Please use '--%s' instead.\n", serviceAccFlag)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
p.Warn("using a positional argument for the service account email is deprecated and will be removed after 2026-12. Please use '--%s' instead.\n", serviceAccFlag)
p.Warn("Using a positional argument for the service account email is deprecated and will be removed after 2026-12. Please use the '--%s' flag 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)
}

model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Expand Down
23 changes: 17 additions & 6 deletions internal/cmd/server/service-account/detach/detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,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 after 2026-12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12
serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12-31

Did you add this to our deprecation document?


serverIdFlag = "server-id"

serviceAccFlag = "service-account-email"
)

type inputModel struct {
Expand All @@ -33,14 +35,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),
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 +87,27 @@ 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 after 2026-12. Please use '--%s' instead.\n", serviceAccFlag)

@rubenhoenle rubenhoenle Jun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
p.Warn("using a positional argument for the service account email is deprecated and will be removed after 2026-12. Please use '--%s' instead.\n", serviceAccFlag)
p.Warn("Using a positional argument for the service account email is deprecated and will be removed after 2026-12-31. Please use the '--%s' flag 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)
}

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