Skip to content
Draft
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
34 changes: 34 additions & 0 deletions cmd/gcp/gcp.go
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 }
32 changes: 32 additions & 0 deletions cmd/gcp/gcp_test.go
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")
}
11 changes: 11 additions & 0 deletions cmd/gcp/gke/gke.go
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,
}
141 changes: 141 additions & 0 deletions cmd/gcp/gke/token.go
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)
Comment on lines +53 to +62

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^cmd/gcp/gke/(token|gke)\.go$|^internal/auth/.*auth.*\.go$|^pkg/auth/.*\.go$|auth\.go$|cmd/.*flags.*\.go$)' || true

echo "== token.go outline =="
ast-grep outline cmd/gcp/gke/token.go || true
echo "== token.go relevant lines =="
sed -n '1,110p' cmd/gcp/gke/token.go

echo "== Authenticate definitions/usages =="
rg -n "func ContextWithSkipIntegrations|ContextWithSkipIntegrations|func .*Authenticate|Authenticate\(" internal pkg cmd --glob '*.go' || true

echo "== standard parser/usages =="
rg -n "flags\.NewStandardParser|NewStandardParser|cmd\.Context\(" cmd --glob '*.go' | head -n 80 || true

Repository: cloudposse/atmos

Length of output: 1918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository status =="
pwd
git status --short || true
git ls-files | head -n 80 || true

echo "== token.go existence and lines =="
if [ -f cmd/gcp/gke/token.go ]; then
  wc -l cmd/gcp/gke/token.go
  sed -n '1,90p' cmd/gcp/gke/token.go
else
  echo "missing cmd/gcp/gke/token.go"
fi

echo "== auth context wrapper definitions/usages =="
rg -n "ContextWithSkipIntegrations|func \([^)]*\) Context|type .*Context|authenticateForTokenFn|func \(.*Authenticate|Authenticate\(" . --glob '*.go' --glob '!vendor/**' | head -n 200 || true

echo "== command context and parser usage =="
rg -n "cmd\.Context\(\)|flags\.NewStandardParser\(|NewStandardParser\(\"" cmd --glob '*.go' | head -n 120 || true

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:createAuthCtx currently replaces Cobra’s lifecycle context with context.Background(), so cancellation and deadlines cannot reach mgr.Authenticate. Use auth.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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/gcp/gke/token.go` around lines 53 - 62, The authentication context in
executeTokenCommand must preserve Cobra’s cancellation and deadline while
retaining skip-integrations behavior. Replace the context.Background() input to
auth.ContextWithSkipIntegrations with cmd.Context(), and add coverage verifying
cancellation/deadline inheritance reaches authenticateForTokenFn.

Source: Coding guidelines

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)
}
Loading
Loading