-
-
Notifications
You must be signed in to change notification settings - Fork 174
feat(auth): add GKE kubeconfig integration #2901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Mikhail Shirkov (shirkevich)
wants to merge
1
commit into
cloudposse:osterman/aks-acr-support
Choose a base branch
from
shirkevich:codex/gcp-gke-auth-integration
base: osterman/aks-acr-support
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package gcp | ||
|
|
||
| import ( | ||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/cloudposse/atmos/cmd/gcp/gke" | ||
| "github.com/cloudposse/atmos/cmd/internal" | ||
| "github.com/cloudposse/atmos/pkg/flags" | ||
| "github.com/cloudposse/atmos/pkg/flags/compat" | ||
| ) | ||
|
|
||
| var gcpCmd = &cobra.Command{ | ||
| Use: "gcp", | ||
| Short: "Run GCP-specific commands for interacting with cloud resources", | ||
| Long: "This command allows interaction with Google Cloud resources through native Atmos commands.", | ||
| Args: cobra.NoArgs, | ||
| } | ||
|
|
||
| func init() { | ||
| gcpCmd.AddCommand(gke.GkeCmd) | ||
| internal.Register(&GCPCommandProvider{}) | ||
| } | ||
|
|
||
| // GCPCommandProvider registers the gcp command. | ||
| type GCPCommandProvider struct{} | ||
|
|
||
| func (*GCPCommandProvider) GetCommand() *cobra.Command { return gcpCmd } | ||
| func (*GCPCommandProvider) GetName() string { return "gcp" } | ||
| func (*GCPCommandProvider) GetGroup() string { return "Cloud Integration" } | ||
| func (*GCPCommandProvider) GetAliases() []internal.CommandAlias { return nil } | ||
| func (*GCPCommandProvider) GetFlagsBuilder() flags.Builder { return nil } | ||
| func (*GCPCommandProvider) GetPositionalArgsBuilder() *flags.PositionalArgsBuilder { return nil } | ||
| func (*GCPCommandProvider) GetCompatibilityFlags() map[string]compat.CompatibilityFlag { return nil } | ||
| func (*GCPCommandProvider) IsExperimental() bool { return false } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package gcp | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestGCPCommandProvider(t *testing.T) { | ||
| provider := &GCPCommandProvider{} | ||
| assert.Equal(t, "gcp", provider.GetName()) | ||
| assert.Equal(t, "Cloud Integration", provider.GetGroup()) | ||
| assert.False(t, provider.IsExperimental()) | ||
| assert.Nil(t, provider.GetAliases()) | ||
| assert.Nil(t, provider.GetFlagsBuilder()) | ||
| assert.Nil(t, provider.GetPositionalArgsBuilder()) | ||
| assert.Nil(t, provider.GetCompatibilityFlags()) | ||
| assert.Same(t, gcpCmd, provider.GetCommand()) | ||
| } | ||
|
|
||
| func TestGCPCommandHierarchyAndHelp(t *testing.T) { | ||
| assert.Equal(t, "gcp", gcpCmd.Use) | ||
| assert.Contains(t, gcpCmd.Short, "GCP-specific") | ||
| gkeCmd, _, err := gcpCmd.Find([]string{"gke"}) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "gke", gkeCmd.Name()) | ||
| token, _, err := gcpCmd.Find([]string{"gke", "token"}) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "token", token.Name()) | ||
| assert.Contains(t, token.Long, "ExecCredential") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package gke | ||
|
|
||
| import "github.com/spf13/cobra" | ||
|
|
||
| // GkeCmd executes gcp gke commands. | ||
| var GkeCmd = &cobra.Command{ | ||
| Use: "gke", | ||
| Short: "Manage GKE authentication", | ||
| Long: "Generate short-lived Google Cloud credentials for GKE through Atmos Auth.", | ||
| Args: cobra.NoArgs, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| package gke | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| errUtils "github.com/cloudposse/atmos/errors" | ||
| "github.com/cloudposse/atmos/pkg/auth" | ||
| gcpCloud "github.com/cloudposse/atmos/pkg/auth/cloud/gcp" | ||
| "github.com/cloudposse/atmos/pkg/auth/credentials" | ||
| "github.com/cloudposse/atmos/pkg/auth/types" | ||
| "github.com/cloudposse/atmos/pkg/auth/validation" | ||
| cfg "github.com/cloudposse/atmos/pkg/config" | ||
| "github.com/cloudposse/atmos/pkg/data" | ||
| "github.com/cloudposse/atmos/pkg/perf" | ||
| "github.com/cloudposse/atmos/pkg/schema" | ||
| ) | ||
|
|
||
| const execCredentialAPIVersion = "client.authentication.k8s.io/v1beta1" | ||
|
|
||
| var ( | ||
| initCliConfigFn = cfg.InitCliConfig | ||
| authenticateForTokenFn = authenticateForToken | ||
| getGKETokenFn = gcpCloud.GetToken | ||
| newAuthManagerFn = auth.NewAuthManager | ||
| ) | ||
|
|
||
| var tokenCmd = &cobra.Command{ | ||
| Use: "token", | ||
| Short: "Generate a GKE bearer token for kubectl", | ||
| Long: "Generate a Kubernetes ExecCredential from an Atmos-managed GCP identity. This command is normally invoked by kubectl from an Atmos-generated kubeconfig.", | ||
| Args: cobra.NoArgs, | ||
| RunE: executeTokenCommand, | ||
| SilenceUsage: true, | ||
| } | ||
|
|
||
| type execCredential struct { | ||
| APIVersion string `json:"apiVersion"` | ||
| Kind string `json:"kind"` | ||
| Status execCredentialStatus `json:"status"` | ||
| } | ||
|
|
||
| type execCredentialStatus struct { | ||
| ExpirationTimestamp string `json:"expirationTimestamp,omitempty"` | ||
| Token string `json:"token"` | ||
| } | ||
|
|
||
| func executeTokenCommand(cmd *cobra.Command, _ []string) error { | ||
| atmosConfig, err := initCliConfigFn(schema.ConfigAndStacksInfo{}, false) | ||
| if err != nil { | ||
| return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitConfig, err) | ||
| } | ||
| defer perf.Track(&atmosConfig, "gke.executeTokenCommand")() | ||
|
|
||
| identityName := resolveIdentity(cmd) | ||
| ctx := auth.ContextWithSkipIntegrations(context.Background()) | ||
| creds, err := authenticateForTokenFn(ctx, &atmosConfig.Auth, atmosConfig.CliConfigPath, identityName) | ||
| if err != nil { | ||
| return fmt.Errorf("%w: %w", errUtils.ErrGKETokenGeneration, err) | ||
| } | ||
| token, expiresAt, err := getGKETokenFn(creds) | ||
| if err != nil { | ||
| return fmt.Errorf("%w: %w", errUtils.ErrGKETokenGeneration, err) | ||
| } | ||
| return writeExecCredential(token, expiresAt) | ||
| } | ||
|
|
||
| func writeExecCredential(token string, expiresAt time.Time) error { | ||
| status := execCredentialStatus{Token: token} | ||
| if !expiresAt.IsZero() { | ||
| status.ExpirationTimestamp = expiresAt.UTC().Format(time.RFC3339) | ||
| } | ||
| payload, err := json.Marshal(execCredential{ | ||
| APIVersion: execCredentialAPIVersion, | ||
| Kind: "ExecCredential", | ||
| Status: status, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("%w: failed to marshal ExecCredential: %w", errUtils.ErrGKETokenGeneration, err) | ||
| } | ||
| return data.Write(string(payload)) | ||
| } | ||
|
|
||
| func resolveIdentity(cmd *cobra.Command) string { | ||
| identityName, _ := cmd.Flags().GetString("identity") | ||
| if identityName != "" { | ||
| return identityName | ||
| } | ||
| return os.Getenv("ATMOS_IDENTITY") //nolint:forbidigo // Exec plugins inherit this explicit identity selector. | ||
| } | ||
|
|
||
| func authenticateForToken(ctx context.Context, authConfig *schema.AuthConfig, cliConfigPath, identityName string) (types.ICredentials, error) { | ||
| authStackInfo := &schema.ConfigAndStacksInfo{AuthContext: &schema.AuthContext{}} | ||
| mgr, err := newAuthManagerFn( | ||
| authConfig, | ||
| credentials.NewCredentialStoreWithConfig(authConfig), | ||
| validation.NewValidator(), | ||
| authStackInfo, | ||
| cliConfigPath, | ||
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitializeAuthManager, err) | ||
| } | ||
| if identityName == "" { | ||
| identityName = resolveDefaultIdentity(authConfig) | ||
| if identityName == "" { | ||
| return nil, fmt.Errorf("%w: no identity specified and no default identity found", errUtils.ErrGKETokenGeneration) | ||
| } | ||
| } | ||
| whoami, err := mgr.Authenticate(ctx, identityName) | ||
| if err != nil { | ||
| return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, err) | ||
| } | ||
| if whoami.Credentials == nil { | ||
| return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, errUtils.ErrIdentityCredentialsNone) | ||
| } | ||
| if _, ok := whoami.Credentials.(*types.GCPCredentials); !ok { | ||
| return nil, fmt.Errorf("%w: identity %q returned non-GCP credentials", errUtils.ErrGKETokenGeneration, identityName) | ||
| } | ||
| return whoami.Credentials, nil | ||
| } | ||
|
|
||
| func resolveDefaultIdentity(authConfig *schema.AuthConfig) string { | ||
| if authConfig == nil || len(authConfig.Identities) != 1 { | ||
| return "" | ||
| } | ||
| for name := range authConfig.Identities { | ||
| return name | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func init() { | ||
| tokenCmd.Flags().StringP("identity", "i", "", "Atmos GCP identity to authenticate with") | ||
| GkeCmd.AddCommand(tokenCmd) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1918
🏁 Script executed:
Repository: cloudposse/atmos
Length of output: 1918
🌐 Web query:
atmos context.ContextWithSkipIntegrations cmd.Context context.Background auth.ContextWithSkipIntegrations💡 Result:
In the context of the Atmos CLI project, these terms relate to context management, authentication workflows, and command-line execution: context.ContextWithSkipIntegrations: This is a function provided by the github.com/cloudposse/atmos/pkg/auth package [1]. It returns a context derived from a parent context that includes a specific key (skipIntegrationsKey) [2][3]. This modified context is used to signal the authentication manager to skip auto-triggered integrations [1][4]. This is typically employed when performing operations like token generation where explicit integration execution is required, and redundant auto-provisioning (such as re-writing kubeconfig) should be avoided [1][4]. cmd.Context: In Atmos, cmd.Context is the standard method for retrieving the context associated with a cobra.Command [5][6]. It is preferred over context.Background because it propagates cancellations and timeouts managed by the CLI framework throughout the command's execution [5]. context.Background: This is the standard Go library function used to return a non-nil, empty context. It is generally intended to be used as a root context in main functions, init, or tests. In the Atmos codebase, it is often replaced by cmd.Context to ensure that lifecycle management (like command cancellation) is respected [5]. In summary, when developing or interacting with Atmos, you should use cmd.Context to maintain CLI lifecycle awareness [5], and use auth.ContextWithSkipIntegrations when you need to perform authentication without triggering automatic side-effect integrations [1][4].
Citations:
🌐 Web query:
github.com/cloudposse/atmos/pkg/auth ContextWithSkipIntegrations source💡 Result:
The function ContextWithSkipIntegrations in the cloudposse/atmos Go package (pkg/auth) is a helper utility used to manage the execution of authentication integrations [1][2]. Function Purpose ContextWithSkipIntegrations returns a new context.Context derived from the provided context that contains a specific flag to skip auto-triggered integrations during the authentication process [1][3]. By adding this to a context, developers can signal the auth manager to bypass standard automated triggers [4]. Technical Details - Implementation: It works by using context.WithValue to attach a private key (skipIntegrationsKey) to the context with a boolean value of true [3][4]. - Use Case: It is primarily used when performing explicit operations where auto-triggered integrations are redundant or undesirable, such as generating tokens or executing specific integrations that are already being managed manually [1][4]. This prevents duplicate execution of integration logic, such as re-provisioning infrastructure or rewriting configuration files (e.g., kubeconfig) when they have already been handled by a caller [1][3][4]. - Integration check: The auth manager's triggerIntegrations method checks for the presence of this key in the context; if it is found, the manager logs the skip and returns early, avoiding the execution of linked integrations [4]. This function was introduced to improve the lifecycle management of authentication environments, specifically handling scenarios where explicit execution and auto-provisioning might overlap [2].
Citations:
Preserve the command cancellation context.
token.go:createAuthCtxcurrently replaces Cobra’s lifecycle context withcontext.Background(), so cancellation and deadlines cannot reachmgr.Authenticate. Useauth.ContextWithSkipIntegrations(cmd.Context())as the command already needs the skip-integrations behavior; add a test for inheritance of the cancellation deadline.🤖 Prompt for AI Agents
Source: Coding guidelines