Add ECR authentication support to Atmos via a new auth.integrations section that provides client-only credential materialization for services like ECR and EKS.
Key Insight: ECR (and EKS) credentials are fundamentally different from identities:
| Concept | IAM User | Docker Login (ECR) | EKS (IAM → kubeconfig) |
|---|---|---|---|
| Stored identity object | ✅ | ❌ | ❌ |
| Policy attachment | ✅ | ❌ | ❌ |
| Stable subject | ✅ | ❌ | ❌ |
| Server-side lifecycle | ✅ | ❌ | ❌ |
| Client-only materialization | ❌ | ✅ | ✅ |
Design Decision: ECR login is implemented as an integration (not an identity) that references an existing identity. This cleanly separates "who you are" (identity) from "derived credentials for services" (integrations).
Users cannot easily authenticate to AWS ECR using Atmos-managed credentials. To pull or push images, they must:
- Manually run
aws ecr get-login-passwordto retrieve authorization tokens - Pipe the output to
docker loginwith the correct registry URL - Repeat this process every 12 hours (ECR token expiration)
- Configure
DOCKER_CONFIGenvironment variable to use isolated credentials - Manage credential refresh across multiple registries and accounts
This creates friction for common workflows:
- Local development requiring ECR image pulls
- GitHub Actions workflows needing ECR access
- Devcontainer builds using private base images
Current Experience:
# User authenticates with Atmos for AWS access
$ atmos auth login dev-admin
# User needs to pull ECR image... must do this manually:
$ aws ecr get-login-password --region us-east-2 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-2.amazonaws.com
# Token expires after 12 hours, must repeat
# Must also configure DOCKER_CONFIG for isolationDesired Experience (Automatic via integration):
# Identity references an integration that auto-logins to ECR
$ atmos auth login dev-admin
✓ Authenticated as arn:aws:sts::123456789012:assumed-role/DevRole/user
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
# Docker commands work automatically
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest
# Success!Desired Experience (Explicit command with integration name):
# Explicitly login to ECR using a named integration
$ atmos auth ecr-login dev/ecr
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest
# Success!Desired Experience (Explicit command with identity flag):
# Explicitly login to ECR using an identity's linked integrations
$ atmos auth ecr-login --identity dev-admin
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest
# Success!- Clean Separation of Concerns: Identities for "who you are", integrations for "derived service credentials"
- Explicit Configuration: Each integration is named and configured independently
- Identity Linking: Integrations reference identities for AWS credentials
- Automatic Login: Identities can trigger integrations via PostAuthenticate hook
- Explicit Control: Standalone command for ad-hoc integration login
- Non-Blocking Errors: Integration failures don't block identity authentication
- Isolated Docker Config: Use Atmos-managed Docker config file
- XDG Compliance: Use
pkg/xdgfor config paths - Multi-Registry Support: Support multiple ECR registries per integration
- Future-Proof: Pattern extends naturally to EKS and other integrations
auth:
# Identities define WHO you are (server-side, policy-attached)
identities:
dev-admin:
kind: aws/permission-set
via:
provider: aws-sso
principal:
name: AdministratorAccess
account:
name: dev
# Integrations define DERIVED credentials (client-only materialization)
# Each integration specifies which identity it uses via "via.identity"
integrations:
dev/ecr/main:
kind: aws/ecr
via:
identity: dev-admin # Which identity provides AWS creds
spec:
auto_provision: true # Auto-trigger on identity login
registry:
account_id: "123456789012"
region: us-east-2
dev/ecr/secondary:
kind: aws/ecr
via:
identity: dev-admin
spec:
auto_provision: true
registry:
account_id: "987654321098"
region: us-west-2
# Future: EKS integration (not implemented in this PRD)
# dev/kubecfg:
# kind: aws/eks
# via:
# identity: dev-admin
# spec:
# cluster:
# name: dev-cluster
# region: us-east-2| Field | Required | Description |
|---|---|---|
kind |
Yes | Integration type (e.g., aws/ecr, future: aws/eks) |
via.identity |
Yes | Name of identity providing AWS credentials |
spec.auto_provision |
No | Auto-trigger on identity login (default: true) |
spec.registry |
Yes | ECR registry configuration |
spec.registry.account_id |
Yes | AWS account ID for registry |
spec.registry.region |
Yes | AWS region for registry |
Each integration defines a single registry rather than a list. This approach:
- Better Deep Merging: Works with Atmos stack inheritance and merging
- Clearer Naming: Integration name reflects its purpose (e.g.,
dev/ecr/main) - Consistent Pattern: Matches how identities and providers are defined
- Easier Override: Individual registries can be overridden in stack configs
# Login using a named integration
atmos auth ecr-login dev/ecr
# Login using an identity's linked integrations
atmos auth ecr-login --identity dev-admin
# Override with explicit registry (uses current AWS credentials)
atmos auth ecr-login --registry 123456789012.dkr.ecr.us-east-2.amazonaws.com
# Multiple explicit registries
atmos auth ecr-login \
--registry 123456789012.dkr.ecr.us-east-2.amazonaws.com \
--registry 987654321098.dkr.ecr.us-west-2.amazonaws.com┌─────────────────────────────────────────────────────────────────┐
│ 1. User Executes Command │
│ $ atmos auth login dev-admin │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. Normal AWS Authentication │
│ - SSO login / assume role / IAM user auth │
│ - Obtain AWS credentials │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. PostAuthenticate Hook │
│ - SetupFiles() - write AWS credential files │
│ - SetAuthContext() - populate in-process auth │
│ - SetEnvironmentVariables() - configure subprocess env │
│ - TriggerIntegrations() - process identity.integrations list │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. For each integration in identity.integrations: │
│ - Look up integration config (auth.integrations.dev/ecr) │
│ - Call integration handler (ECR login) │
│ - Log success/warning (non-fatal errors) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Return Success │
│ ✓ Authenticated as arn:aws:sts::... │
│ ✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 1. User Executes Command │
│ $ atmos auth ecr-login dev/ecr │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. Load Integration Config │
│ - Look up auth.integrations.dev/ecr │
│ - Get identity reference (dev-admin) │
│ - Get registry list │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Authenticate Referenced Identity │
│ - Authenticate dev-admin identity │
│ - Obtain AWS credentials │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. ECR Login │
│ - Call ecr:GetAuthorizationToken for each registry │
│ - Write to Atmos Docker config │
│ - Set DOCKER_CONFIG environment variable │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Return Success │
│ ✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com │
└─────────────────────────────────────────────────────────────────┘
pkg/auth/
├── cloud/
│ ├── aws/
│ │ └── ecr.go # ECR token fetcher
│ └── docker/
│ └── config.go # Docker config.json manager
├── integrations/
│ ├── registry.go # Integration type registry
│ ├── types.go # Integration interfaces
│ └── aws/
│ └── ecr.go # ECR integration implementation
└── ecr_login.go # Standalone command implementation
cmd/auth/
└── ecr_login.go # atmos auth ecr-login command
File: pkg/auth/integrations/types.go
// Integration represents a client-only credential materialization.
type Integration interface {
// Kind returns the integration type (e.g., "aws/ecr").
Kind() string
// Execute performs the integration using the provided AWS credentials.
Execute(ctx context.Context, creds *types.AWSCredentials) error
}
// IntegrationConfig is the configuration for an integration.
type IntegrationConfig struct {
Kind string `mapstructure:"kind"`
Identity string `mapstructure:"identity"`
Config map[string]interface{} `mapstructure:",remain"`
}
// IntegrationFactory creates integrations from configuration.
type IntegrationFactory func(config *IntegrationConfig) (Integration, error)File: pkg/auth/integrations/registry.go
// Registry holds registered integration factories.
var registry = map[string]IntegrationFactory{}
// Register adds an integration factory for a kind.
func Register(kind string, factory IntegrationFactory) {
registry[kind] = factory
}
// Create instantiates an integration from config.
func Create(config *IntegrationConfig) (Integration, error) {
factory, ok := registry[config.Kind]
if !ok {
return nil, fmt.Errorf("unknown integration kind: %s", config.Kind)
}
return factory(config)
}
// Integration kind constants.
const (
KindAWSECR = "aws/ecr"
KindAWSEKS = "aws/eks" // Future
)
func init() {
Register(KindAWSECR, NewECRIntegration)
}File: pkg/auth/integrations/aws/ecr.go
// ECRIntegration implements the aws/ecr integration type.
type ECRIntegration struct {
identity string
registries []ECRRegistry
}
// ECRRegistry represents a single ECR registry configuration.
type ECRRegistry struct {
AccountID string `mapstructure:"account_id"`
Region string `mapstructure:"region"`
}
// NewECRIntegration creates an ECR integration from config.
func NewECRIntegration(config *IntegrationConfig) (Integration, error) {
var registries []ECRRegistry
if err := mapstructure.Decode(config.Config["registries"], ®istries); err != nil {
return nil, fmt.Errorf("invalid registries config: %w", err)
}
return &ECRIntegration{
identity: config.Identity,
registries: registries,
}, nil
}
// Kind returns "aws/ecr".
func (e *ECRIntegration) Kind() string {
return integrations.KindAWSECR
}
// Execute performs ECR login for all configured registries.
func (e *ECRIntegration) Execute(ctx context.Context, creds *types.AWSCredentials) error {
dockerConfig, err := docker.NewConfigManager()
if err != nil {
return err
}
awsCfg, err := buildAWSConfig(ctx, creds)
if err != nil {
return err
}
for _, reg := range e.registries {
result, err := awsCloud.GetAuthorizationToken(ctx, awsCfg, reg.AccountID, reg.Region)
if err != nil {
return fmt.Errorf("ECR login failed for %s: %w", reg.AccountID, err)
}
if err := dockerConfig.WriteAuth(result.Registry, result.Username, result.Password); err != nil {
return fmt.Errorf("failed to write Docker config: %w", err)
}
ui.Success("ECR login: %s (expires in 12h)", result.Registry)
}
os.Setenv("DOCKER_CONFIG", dockerConfig.GetConfigDir())
return nil
}File: pkg/auth/cloud/docker/config.go
// ConfigManager manages Docker config.json for ECR authentication.
// It uses file locking to prevent concurrent modification.
type ConfigManager struct {
configDir string
configPath string
mu sync.Mutex
}
// NewConfigManager creates a new Docker config manager using the default Docker config path.
// The config is stored in ~/.docker/config.json by default (or $DOCKER_CONFIG/config.json
// if DOCKER_CONFIG is set). This ensures compatibility with Docker CLI without requiring
// additional environment variable configuration.
func NewConfigManager() (*ConfigManager, error) {
configDir := getDockerConfigDir()
// Ensure directory exists with secure permissions.
if err := os.MkdirAll(configDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create docker config directory: %w", err)
}
return &ConfigManager{
configDir: configDir,
configPath: filepath.Join(configDir, "config.json"),
}, nil
}
// getDockerConfigDir returns the Docker config directory.
// Uses DOCKER_CONFIG environment variable if set, otherwise defaults to ~/.docker.
func getDockerConfigDir() string {
// Bind and read DOCKER_CONFIG environment variable via viper.
_ = viper.BindEnv("DOCKER_CONFIG")
if dockerConfig := viper.GetString("DOCKER_CONFIG"); dockerConfig != "" {
return dockerConfig
}
homeDir, err := homedir.Dir()
if err != nil {
return ".docker"
}
return filepath.Join(homeDir, ".docker")
}
// WriteAuth writes ECR authorization to Docker config.
func (m *ConfigManager) WriteAuth(registry string, username string, password string) error
// RemoveAuth removes ECR authorization from Docker config.
func (m *ConfigManager) RemoveAuth(registries ...string) error
// GetConfigDir returns the directory containing the Docker config.
func (m *ConfigManager) GetConfigDir() string
// GetAuthenticatedRegistries returns list of authenticated ECR registries.
func (m *ConfigManager) GetAuthenticatedRegistries() ([]string, error)Docker Config Format:
{
"auths": {
"123456789012.dkr.ecr.us-east-2.amazonaws.com": {
"auth": "QVdTOmV5SjBlWEJsLi4u"
}
}
}The auth field contains base64(username:password) where username is always AWS and password is the authorization token from ECR.
File: pkg/auth/cloud/aws/ecr.go
// ECRAuthResult contains ECR authorization token information.
type ECRAuthResult struct {
Username string // Always "AWS"
Password string // Decoded authorization token
Registry string // e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com
ExpiresAt time.Time // Token expiration time
}
// GetAuthorizationToken retrieves ECR credentials using AWS config.
func GetAuthorizationToken(ctx context.Context, cfg aws.Config, accountID, region string) (*ECRAuthResult, error)
// BuildRegistryURL constructs ECR registry URL from account ID and region.
func BuildRegistryURL(accountID, region string) string
// ParseRegistryURL extracts account ID and region from ECR registry URL.
func ParseRegistryURL(registryURL string) (accountID, region string, err error)In each identity's PostAuthenticate() method, add integration trigger support:
File: pkg/auth/identities/aws/*.go
func (i *userIdentity) PostAuthenticate(ctx context.Context, params *types.PostAuthenticateParams) error {
// Existing code...
if err := awsCloud.SetupFiles(...); err != nil { /* handle */ }
if err := awsCloud.SetAuthContext(...); err != nil { /* handle */ }
if err := awsCloud.SetEnvironmentVariables(...); err != nil { /* handle */ }
// NEW: Trigger linked integrations
for _, integrationName := range i.config.Integrations {
integrationConfig, err := params.AuthConfig.GetIntegration(integrationName)
if err != nil {
log.Warn("Failed to find integration", "name", integrationName, "error", err)
continue
}
integration, err := integrations.Create(integrationConfig)
if err != nil {
log.Warn("Failed to create integration", "name", integrationName, "error", err)
continue
}
if err := integration.Execute(ctx, params.Credentials); err != nil {
log.Warn("Integration failed", "name", integrationName, "error", err)
// Non-fatal - don't block authentication
}
}
return nil
}File: cmd/auth/ecr_login.go
var ecrLoginCmd = &cobra.Command{
Use: "ecr-login [integration]",
Short: "Login to AWS ECR registries",
Long: `Login to AWS ECR registries using a named integration or identity.
Examples:
# Login using a named integration
atmos auth ecr-login dev/ecr
# Login using an identity's linked integrations
atmos auth ecr-login --identity dev-admin
# Override with explicit registry URL
atmos auth ecr-login --registry 123456789012.dkr.ecr.us-east-2.amazonaws.com`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var integrationName string
if len(args) > 0 {
integrationName = args[0]
}
identity, _ := cmd.Flags().GetString("identity")
registries, _ := cmd.Flags().GetStringArray("registry")
return auth.ECRLogin(ctx, integrationName, identity, registries)
},
}
func init() {
ecrLoginCmd.Flags().StringP("identity", "i", "", "Identity to use (triggers its linked integrations)")
ecrLoginCmd.Flags().StringArrayP("registry", "r", nil, "ECR registry URL(s) - explicit mode")
authCmd.AddCommand(ecrLoginCmd)
}File: pkg/auth/ecr_login.go
// ECRLogin performs ECR authentication.
// Priority: integrationName > identity > registries (explicit mode).
func ECRLogin(ctx context.Context, integrationName, identityName string, registries []string) error {
authManager, err := createAuthManager()
if err != nil {
return err
}
// Case 1: Named integration
if integrationName != "" {
return executeIntegration(ctx, authManager, integrationName)
}
// Case 2: Identity's linked integrations
if identityName != "" {
return executeIdentityIntegrations(ctx, authManager, identityName)
}
// Case 3: Explicit registries with current AWS credentials
if len(registries) > 0 {
return executeExplicitRegistries(ctx, registries)
}
return fmt.Errorf("specify an integration name, --identity, or --registry")
}
func executeIntegration(ctx context.Context, authManager *AuthManager, name string) error {
integrationConfig, err := authManager.GetIntegration(name)
if err != nil {
return fmt.Errorf("integration not found: %s", name)
}
// Authenticate the referenced identity
whoami, err := authManager.Authenticate(ctx, integrationConfig.Identity)
if err != nil {
return fmt.Errorf("failed to authenticate identity '%s': %w", integrationConfig.Identity, err)
}
// Create and execute the integration
integration, err := integrations.Create(integrationConfig)
if err != nil {
return err
}
return integration.Execute(ctx, whoami.Credentials)
}
func executeIdentityIntegrations(ctx context.Context, authManager *AuthManager, identityName string) error {
identityConfig, err := authManager.GetIdentity(identityName)
if err != nil {
return fmt.Errorf("identity not found: %s", identityName)
}
if len(identityConfig.Integrations) == 0 {
return fmt.Errorf("identity '%s' has no linked integrations", identityName)
}
// Authenticate the identity
whoami, err := authManager.Authenticate(ctx, identityName)
if err != nil {
return fmt.Errorf("failed to authenticate identity '%s': %w", identityName, err)
}
// Execute each linked integration
for _, integrationName := range identityConfig.Integrations {
integrationConfig, err := authManager.GetIntegration(integrationName)
if err != nil {
return fmt.Errorf("integration not found: %s", integrationName)
}
integration, err := integrations.Create(integrationConfig)
if err != nil {
return err
}
if err := integration.Execute(ctx, whoami.Credentials); err != nil {
return err
}
}
return nil
}
func executeExplicitRegistries(ctx context.Context, registries []string) error {
// Use default AWS credential chain
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return fmt.Errorf("failed to load AWS config: %w", err)
}
dockerConfig, err := docker.NewConfigManager()
if err != nil {
return err
}
for _, registry := range registries {
accountID, region, err := awsCloud.ParseRegistryURL(registry)
if err != nil {
return fmt.Errorf("invalid registry URL %s: %w", registry, err)
}
result, err := awsCloud.GetAuthorizationToken(ctx, awsCfg, accountID, region)
if err != nil {
return fmt.Errorf("ECR login failed for %s: %w", registry, err)
}
if err := dockerConfig.WriteAuth(result.Registry, result.Username, result.Password); err != nil {
return fmt.Errorf("failed to write Docker config: %w", err)
}
ui.Success("ECR login: %s (expires in 12h)", result.Registry)
}
os.Setenv("DOCKER_CONFIG", dockerConfig.GetConfigDir())
return nil
}Add sentinel errors to errors/errors.go:
// ECR authentication errors.
var (
ErrECRAuthenticationFailed = errors.New("ECR authentication failed")
ErrECRTokenExpired = errors.New("ECR authorization token expired")
ErrECRRegistryNotFound = errors.New("ECR registry not found")
ErrIntegrationNotFound = errors.New("integration not found")
ErrUnknownIntegrationKind = errors.New("unknown integration kind")
)Error Behavior:
| Context | Error Behavior |
|---|---|
| PostAuthenticate hook | Non-fatal: log warning, continue identity auth |
| Standalone command | Fatal: return error to user |
ECR credentials are written directly to the standard Docker config location, so no additional environment variables are required. Docker commands work immediately after ECR login.
| Variable | When Set | Purpose |
|---|---|---|
DOCKER_CONFIG |
User-defined | If set, ECR credentials are written to $DOCKER_CONFIG/config.json instead of ~/.docker/config.json |
The Docker config manager uses file locking to prevent concurrent modification:
import "github.com/gofrs/flock"
func (m *ConfigManager) WriteAuth(...) error {
lock := flock.New(m.configPath + ".lock")
if err := lock.Lock(); err != nil {
return err
}
defer lock.Unlock()
// ... write config ...
}# atmos.yaml
auth:
identities:
dev-admin:
kind: aws/permission-set
via:
provider: aws-sso
principal:
name: AdministratorAccess
account:
name: dev
integrations:
dev/ecr:
kind: aws/ecr
via:
identity: dev-admin
spec:
auto_provision: true
registry:
account_id: "123456789012"
region: us-east-2$ atmos auth login dev-admin
✓ Authenticated as arn:aws:sts::123456789012:assumed-role/DevRole/user
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
# Docker commands work automatically
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest
latest: Pulling from my-app
...
Status: Downloaded newer image for my-app:latest# Login using a named integration
$ atmos auth ecr-login dev/ecr
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest# Login using an identity's linked integrations
$ atmos auth ecr-login --identity dev-admin
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
$ docker pull 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:latest# Override with explicit registry URL (uses current AWS credentials)
$ atmos auth ecr-login --registry 123456789012.dkr.ecr.us-east-2.amazonaws.com
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)# atmos.yaml
auth:
integrations:
all-envs/ecr/primary:
kind: aws/ecr
via:
identity: devops-admin
spec:
auto_provision: true
registry:
account_id: "123456789012"
region: us-east-2
all-envs/ecr/secondary:
kind: aws/ecr
via:
identity: devops-admin
spec:
auto_provision: true
registry:
account_id: "987654321098"
region: us-west-2# All integrations with auto_provision=true for devops-admin are triggered
$ atmos auth login devops-admin
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)
✓ ECR login: 987654321098.dkr.ecr.us-west-2.amazonaws.com (expires in 12h)
# Or explicitly trigger specific integration
$ atmos auth ecr-login all-envs/ecr/primary
✓ ECR login: 123456789012.dkr.ecr.us-east-2.amazonaws.com (expires in 12h)# .github/workflows/build.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Atmos Auth
run: |
# Option A: Identity login triggers ECR via linked integration
atmos auth login aws-ci
# Option B: Explicit integration login
atmos auth ecr-login ci/ecr
- name: Build and Push
run: |
docker build -t 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:${{ github.sha }} .
docker push 123456789012.dkr.ecr.us-east-2.amazonaws.com/my-app:${{ github.sha }}- Add integration type system (
pkg/auth/integrations/types.go) - Add integration registry (
pkg/auth/integrations/registry.go) - Add Docker config manager (
pkg/auth/cloud/docker/config.go) - Add ECR token fetcher (
pkg/auth/cloud/aws/ecr.go) - Add sentinel errors (
errors/errors.go)
- Add ECR integration (
pkg/auth/integrations/aws/ecr.go) - Register ECR integration in registry
- Update schema for
auth.integrationssection - Implement
findIntegrationsForIdentity()to find integrations referencing an identity - Modify manager to trigger integrations after authentication
- Add
cmd/auth_ecr_login.go - Implement
executeExplicitRegistries()for ad-hoc registry login
- Unit tests for integration registry (
pkg/auth/integrations/registry_test.go) - Unit tests for Docker config manager (
pkg/auth/cloud/docker/config_test.go) - Unit tests for ECR token fetcher (
pkg/auth/cloud/aws/ecr_test.go) - Unit tests for ECR integration (
pkg/auth/integrations/aws/ecr_test.go) - Unit tests for manager integration methods (
pkg/auth/manager_integrations_test.go) - Unit tests for
atmos auth ecr-logincommand (cmd/auth_ecr_login_test.go)
- Update
website/docs/cli/commands/auth/login.mdx - Add
website/docs/cli/commands/auth/ecr-login.mdx - Add integration configuration examples to auth docs
- ✅
auth.integrationsschema validated and documented - ✅
atmos auth login <identity>with linked integrations triggers ECR login - ✅
atmos auth ecr-login <integration>works with named integration - ✅
atmos auth ecr-login --identity <name>triggers identity's integrations - ✅
atmos auth ecr-login --registry <url>works with explicit registries - ✅ Docker commands use Atmos-managed credentials via
DOCKER_CONFIG - ✅ Multi-registry support works correctly
- ✅ Integration failures don't block identity authentication
- ✅ Tests achieve >80% coverage
- ✅ Documentation includes usage examples
- Standard Docker Config: ECR credentials are written to the standard Docker config location (
~/.docker/config.jsonor$DOCKER_CONFIG/config.json), ensuring compatibility with Docker CLI and container tools without additional configuration. - File Permissions: Docker config directory created with
0700permissions, config file with0600permissions. - File Locking: Uses
gofrs/flockfor concurrent access safety to prevent race conditions. - Token Lifetime: ECR tokens expire after 12 hours (AWS-enforced). Expiration time is displayed to users.
- Non-Fatal Errors: Integration failures during identity authentication are logged as warnings but don't block the identity authentication flow.
- No Secrets in Logs: Authorization tokens are never logged.
- Secret Masking: ECR tokens follow Atmos secret masking patterns via Gitleaks integration.
The implementation follows Atmos codebase standards and linter requirements:
- Uses AWS SDK for Go v2 (
github.com/aws/aws-sdk-go-v2) - RegistryIds Deprecation: The
GetAuthorizationTokenAPI'sRegistryIdsparameter is deprecated. The implementation no longer specifies registry IDs - the returned token works for any ECR registry the IAM credentials have access to.
- AWS SDK imports are restricted to
pkg/auth/cloud/aws/packages (enforced by depguard) cmd/packages use helper functions frompkg/auth/cloud/aws/rather than importing AWS SDK directly- Environment variable access uses
viper.BindEnv()instead of directos.Getenv()calls - Home directory resolution uses
homedir.Dir()frompkg/config/homedirfor cross-platform support.
Sentinel errors defined in errors/errors.go:
// ECR authentication errors.
ErrECRAuthFailed = errors.New("ECR authentication failed")
ErrECRTokenExpired = errors.New("ECR authorization token expired")
ErrECRRegistryNotFound = errors.New("ECR registry not found")
ErrECRInvalidRegistry = errors.New("invalid ECR registry URL")
ErrECRLoginNoArgs = errors.New("specify an integration name, --identity, or --registry")
// Identity authentication errors (used by integrations).
ErrIdentityAuthFailed = errors.New("failed to authenticate identity")
ErrIdentityCredentialsNone = errors.New("credentials not available for identity")
// Integration errors.
ErrIntegrationNotFound = errors.New("integration not found")
ErrUnknownIntegrationKind = errors.New("unknown integration kind")
ErrIntegrationFailed = errors.New("integration execution failed")
ErrNoLinkedIntegrations = errors.New("identity has no linked integrations")- All comments end with periods (godot linter)
- Functions use wrapped static errors (err113 linter)
- File locking uses
gofrs/flockfor concurrent access safety
The integrations pattern naturally extends to other client-only credential materializations:
auth:
integrations:
dev/kubecfg:
kind: aws/eks
via:
identity: dev-admin
spec:
auto_provision: true
cluster:
name: dev-cluster
region: us-east-2
alias: dev # Optional: kubeconfig context nameauth:
integrations:
dev/gcr:
kind: gcp/artifact-registry
via:
identity: gcp-dev
spec:
auto_provision: true
registry:
project: my-project
location: us-central1