From f94bc10524a887e754b25116205ac7ffdff07dc6 Mon Sep 17 00:00:00 2001 From: sanjog-lama Date: Wed, 25 Jun 2025 11:01:56 +0545 Subject: [PATCH 01/51] feat: added version flag compatibility on the root command --- cmd/root/root.go | 3 +++ main.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cmd/root/root.go b/cmd/root/root.go index 1742c14..ab8ae59 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -27,6 +27,7 @@ type RootDependencies struct { RDSService rds.RDSServiceInterface EKSService eks.EKSServiceInterface ECRService ecr.ECRServiceInterface + Version string } func NewRootCmd(deps RootDependencies) *cobra.Command { @@ -37,7 +38,9 @@ func NewRootCmd(deps RootDependencies) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, + Version: deps.Version, } + rootCmd.SetVersionTemplate(`{{printf "%s version %s\n" .Name .Version}}`) rootCmd.AddCommand(cmdSSO.NewSSOCommands(cmdSSO.SSODependencies{ SetupClient: deps.SSOSetupClient, diff --git a/main.go b/main.go index 21d375a..35afb96 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm" ) +var Version = "0.2.0" + func main() { ssoSetupClient, err := sso.NewSSOClient(sso.NewPrompter(), &common.RealCommandExecutor{}) @@ -95,6 +97,7 @@ func main() { RDSService: rdsSvc, EKSService: eksSvc, ECRService: ecrSvc, + Version: Version, }) if err := rootCmd.Execute(); err != nil { os.Exit(1) From f45af409453e635ac4c144f73a26f38f71a49fc8 Mon Sep 17 00:00:00 2001 From: sanjog-lama Date: Wed, 25 Jun 2025 13:09:14 +0545 Subject: [PATCH 02/51] chore: changed reviewer to sarose sir instead of mousam --- .github/workflows/releaser.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index 5ab76f9..81939f2 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -168,7 +168,7 @@ jobs: --head ${{ steps.create_branch.outputs.branch_name }} \ --title "Release ${{ steps.new_version.outputs.new_tag }}" \ --body "Automated release PR for ${{ steps.new_version.outputs.new_tag }}" \ - --reviewer mousamdahal || echo "PR creation failed, possibly no changes or reviewer issue" + --reviewer sarosejoshi || echo "PR creation failed, possibly no changes or reviewer issue" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From eb05661a955e6a94d997231cc3935e483c830b90 Mon Sep 17 00:00:00 2001 From: sanjog-lama Date: Wed, 25 Jun 2025 13:17:03 +0545 Subject: [PATCH 03/51] chore: pre-release test [release] From 7b73cf2c6b50e132029dd5190ab00c019944eab0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 25 Jun 2025 07:36:39 +0000 Subject: [PATCH 04/51] Update CHANGELOG.md for v0.3.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a22aa63..347ab7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +# awsctl - v0.3.0 + +## Changes since v0.2.0 + +- [f45af40](https://github.com/BerryBytes/awsctl/commit/f45af409453e635ac4c144f73a26f38f71a49fc8) Changed reviewer to sarose sir instead of mousam (sanjog-lama ) +- [f94bc10](https://github.com/BerryBytes/awsctl/commit/f94bc10524a887e754b25116205ac7ffdff07dc6) Added version flag compatibility on the root command (sanjog-lama ) +- [1546539](https://github.com/BerryBytes/awsctl/commit/1546539c2f772929a98ff02e0bf58e4c904f2751) Removed zocli instance (sanjog-lama ) +- [0cdf2fe](https://github.com/BerryBytes/awsctl/commit/0cdf2fe73325efed69cc57baae2e530f14017125) Fixed the rds instance listing prompt issue of re-rendering (sanjog-lama ) +- [ca7caae](https://github.com/BerryBytes/awsctl/commit/ca7caaeb38dacca5c4ed896708554f7c79dc7ace) Added commit msg format in submitting PR docs (sanjog-lama ) +- [772ef34](https://github.com/BerryBytes/awsctl/commit/772ef349b09688367a56f00fc5fa183864d12b7b) Changed readme for first time installation of awsctl (sanjog-lama ) +- [ffd2350](https://github.com/BerryBytes/awsctl/commit/ffd2350dc2935955dd5b0c69a20d1e6e14f96d1e) Fixed region validation on setup cmd and double prompt on init cmd (sanjog-lama ) +- [a3c5ada](https://github.com/BerryBytes/awsctl/commit/a3c5ada58d7f2f274097603e19d7d2fe229cfa79) Added coderabbit integration info on readme (sanjog-lama ) +- [b19db49](https://github.com/BerryBytes/awsctl/commit/b19db4995c9cb73890e084c305db4e16eb9ce9dc) Changed commands.md for vpc endpoint for private instance (sanjog-lama ) +- [1d02fd5](https://github.com/BerryBytes/awsctl/commit/1d02fd5476fd13f3472a6740536452bb9541e11a) Included requirement for ec2 instance connect in commands.md (sanjog-lama ) +- [d7f5895](https://github.com/BerryBytes/awsctl/commit/d7f589522e1b0960507a1b4cb9461ff82f7db3b8) Added commands.md for details command usage and linked on readme (sanjog-lama ) +- [2a29cd6](https://github.com/BerryBytes/awsctl/commit/2a29cd66e780749fad6c382436b6c82a2754381e) Changed the command description on readme (sanjog-lama ) +- [4445f7b](https://github.com/BerryBytes/awsctl/commit/4445f7bd0a039880b0ff2d77a0318755a31a5a16) Remove the unwanted test files from previous sso flow (sanjog-lama ) +- [a7953f6](https://github.com/BerryBytes/awsctl/commit/a7953f681e905cb512a238ede487b0f9a2c76be4) Fixed access issue on rds while selecting profile for multiple profile (sanjog-lama ) +- [81f5e12](https://github.com/BerryBytes/awsctl/commit/81f5e12930949152f7a3703c1cebdb6d037036cf) Access issue on eks command while selecting profile (sanjog-lama ) +- [5b3212c](https://github.com/BerryBytes/awsctl/commit/5b3212c416f9b679f31ad46f8754ab27683e08f9) Silence the temp file removal (sanjog-lama ) +- [b7484d2](https://github.com/BerryBytes/awsctl/commit/b7484d2b558559b9b99af0b0ed75356ef0ea5c62) Fix golanci-lint issue (sanjog-lama ) +- [dccf4da](https://github.com/BerryBytes/awsctl/commit/dccf4da5aeef9af26935b04d5004012e08b40cb0) Fix golangci-lint issue (sanjog-lama ) +- [3a26940](https://github.com/BerryBytes/awsctl/commit/3a26940c1240d74da8bd40c1150b9e000aa3161e) Fixed golangci-lint issue (sanjog-lama ) +- [0ee5915](https://github.com/BerryBytes/awsctl/commit/0ee5915bc720b4d4544f6f67bcc392969ccf9e60) Fixed golangci-lint issue of github action (sanjog-lama ) +- [6f69fbd](https://github.com/BerryBytes/awsctl/commit/6f69fbde92cdc38048ca0d56f6c8502aeb2233f9) Added option to set default profile on sso init command (sanjog-lama ) +- [078340c](https://github.com/BerryBytes/awsctl/commit/078340c928b03c0a1e345647bde5d3ffb1866c51) Refactor code suggested by coderabbit (sanjog-lama ) +- [e97d547](https://github.com/BerryBytes/awsctl/commit/e97d547e7c406e9d6c7bd97cf1ea791abd292b7b) Fixed strict checking of region when user is authenticated (sanjog-lama ) +- [4c54d09](https://github.com/BerryBytes/awsctl/commit/4c54d094800ef3d078a5dca2f37be69dfefa9ac6) Added region validation for eks command (sanjog-lama ) +- [54624d6](https://github.com/BerryBytes/awsctl/commit/54624d6b04431cf051e8ef66a3a2a0767aea8da0) Handled user terminated error on manual cluster input (sanjog-lama ) +- [b834f91](https://github.com/BerryBytes/awsctl/commit/b834f91f2f9de35b893b538b3a2c01af9f9d97bf) Added input validation for manual update of eks cluster (sanjog-lama ) +- [d2e4a66](https://github.com/BerryBytes/awsctl/commit/d2e4a667b64fd2a4aa7d5d8e3f202ed3c96f6c35) Changed the config file to new structure and installation command to develop branch (sanjog-lama ) +- [0377e87](https://github.com/BerryBytes/awsctl/commit/0377e87c89d475bb755b31a731b71e1a8f338ca5) Added test case for prompter and changed the related file (sanjog-lama ) +- [a70ce84](https://github.com/BerryBytes/awsctl/commit/a70ce84ef43de9b436bd9b29ba739b7d061d5116) Added test cases for session.go file (sanjog-lama ) +- [9b26697](https://github.com/BerryBytes/awsctl/commit/9b266975271a67c39567d19d31d741a06a984263) Removed unused function from config (sanjog-lama ) +- [d0cdbe1](https://github.com/BerryBytes/awsctl/commit/d0cdbe10fde5cb273a3dff6d92368fee37fd8943) Changed the workflow of sso setup and init command and related files (sanjog-lama ) + +Generated on 2025-06-25T07:36:37Z + # awsctl - v0.2.0 ## Changes since v0.1.0 From 0306d0ed33239873aa626b0d624b9fa4f913e0ff Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 1 Jul 2025 14:45:11 +0545 Subject: [PATCH 05/51] feat: increased the sso login timelimit to 10 minutes from 30 sec --- internal/common/services.go | 2 +- internal/sso/client.go | 2 +- internal/sso/session.go | 2 +- utils/common/common.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/common/services.go b/internal/common/services.go index 6a01fb5..d2fb5b4 100644 --- a/internal/common/services.go +++ b/internal/common/services.go @@ -221,7 +221,7 @@ func (s *Services) IsAWSConfigured() bool { return false } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() creds, err := s.Provider.AwsConfig.Credentials.Retrieve(ctx) diff --git a/internal/sso/client.go b/internal/sso/client.go index 112b0ba..a4fa885 100644 --- a/internal/sso/client.go +++ b/internal/sso/client.go @@ -177,7 +177,7 @@ func (c *RealSSOClient) SSOLogin(awsProfile string, refresh, noBrowser bool) err } args = append(args, "--profile", awsProfile) - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() err := c.Executor.RunInteractiveCommand(ctx, "aws", args...) diff --git a/internal/sso/session.go b/internal/sso/session.go index 0be21fd..5a0f0fe 100644 --- a/internal/sso/session.go +++ b/internal/sso/session.go @@ -235,7 +235,7 @@ func (c *RealSSOClient) runSSOLogin(sessionName string) error { fmt.Println("\nInitiating AWS SSO login... (this may open a browser window)") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() if err := c.Executor.RunInteractiveCommand(ctx, "aws", "sso", "login", "--sso-session", sessionName); err != nil { diff --git a/utils/common/common.go b/utils/common/common.go index 1e44f7e..e56234f 100644 --- a/utils/common/common.go +++ b/utils/common/common.go @@ -71,7 +71,7 @@ func terminateProcess(pid int) error { } } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for { select { From fe9001d2660783fc81b7c92b422365463a7c1364 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 1 Jul 2025 17:07:29 +0545 Subject: [PATCH 06/51] feat: on sso setup, removed extra prompt and setup via start url and region --- internal/sso/session.go | 9 +++------ internal/sso/session_test.go | 35 ++++------------------------------- internal/sso/sso.go | 28 ++++++++-------------------- 3 files changed, 15 insertions(+), 57 deletions(-) diff --git a/internal/sso/session.go b/internal/sso/session.go index 0be21fd..4c472b8 100644 --- a/internal/sso/session.go +++ b/internal/sso/session.go @@ -61,15 +61,12 @@ func (c *RealSSOClient) loadOrCreateSession() (string, *models.SSOSession, error return "", nil, fmt.Errorf("failed to prompt for SSO start URL: %w", err) } - region, err := c.Prompter.PromptForRegion("ap-south-1") + region, err := c.Prompter.PromptForRegion("us-east-1") if err != nil { return "", nil, fmt.Errorf("failed to prompt for SSO region: %w", err) } - scopes, err := c.Prompter.PromptWithDefault("SSO registration scopes (comma separated)", "sso:account:access") - if err != nil { - return "", nil, fmt.Errorf("failed to prompt for SSO scopes: %w", err) - } + scopes := "sso:account:access" ssoSession = &models.SSOSession{ Name: name, @@ -235,7 +232,7 @@ func (c *RealSSOClient) runSSOLogin(sessionName string) error { fmt.Println("\nInitiating AWS SSO login... (this may open a browser window)") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() if err := c.Executor.RunInteractiveCommand(ctx, "aws", "sso", "login", "--sso-session", sessionName); err != nil { diff --git a/internal/sso/session_test.go b/internal/sso/session_test.go index 60ed80f..95cbc7c 100644 --- a/internal/sso/session_test.go +++ b/internal/sso/session_test.go @@ -43,8 +43,7 @@ func TestLoadOrCreateSession(t *testing.T) { mockPrompts: []mockPrompt{ {"PromptWithDefault", "SSO session name", "default-sso", "test-session", nil}, {"PromptRequired", "SSO start URL (e.g., https://my-sso-portal.awsapps.com/start)", "", "https://test.awsapps.com/start", nil}, - {"PromptForRegion", "SSO region (Default: ap-south-1):", "ap-south-1", "us-west-2", nil}, - {"PromptWithDefault", "SSO registration scopes (comma separated)", "sso:account:access", "sso:account:access", nil}, + {"PromptForRegion", "us-east-1", "us-east-1", "us-west-2", nil}, }, wantSession: &models.SSOSession{ Name: "test-session", @@ -54,30 +53,7 @@ func TestLoadOrCreateSession(t *testing.T) { }, wantConfigPath: "", }, - { - name: "Use existing single session - exact name match", - initialConfig: &models.Config{ - SSOSessions: []models.SSOSession{ - { - Name: "existing-session", - StartURL: "https://existing.awsapps.com/start", - Region: "us-east-1", - }, - }, - }, - mockPrompts: []mockPrompt{ - {"PromptWithDefault", "SSO session name", "default-sso", "existing-session", nil}, - {"PromptRequired", "SSO start URL (e.g., https://my-sso-portal.awsapps.com/start)", "", "https://existing.awsapps.com/start", nil}, - {"PromptForRegion", "SSO region (Default: ap-south-1):", "ap-south-1", "us-east-1", nil}, - {"PromptWithDefault", "SSO registration scopes (comma separated)", "sso:account:access", "sso:account:access", nil}, - }, - wantSession: &models.SSOSession{ - Name: "existing-session", - StartURL: "https://existing.awsapps.com/start", - Region: "us-east-1", - Scopes: "sso:account:access", - }, - }, + { name: "Region prompt error", initialConfig: &models.Config{ @@ -86,7 +62,7 @@ func TestLoadOrCreateSession(t *testing.T) { mockPrompts: []mockPrompt{ {"PromptWithDefault", "SSO session name", "default-sso", "test-session", nil}, {"PromptRequired", "SSO start URL (e.g., https://my-sso-portal.awsapps.com/start)", "", "https://test.awsapps.com/start", nil}, - {"PromptForRegion", "SSO region (Default: ap-south-1):", "ap-south-1", "", errors.New("invalid region")}, + {"PromptForRegion", "us-east-1", "us-east-1", "", errors.New("invalid region")}, }, wantErr: true, errContains: "failed to prompt for SSO region", @@ -100,6 +76,7 @@ func TestLoadOrCreateSession(t *testing.T) { mockPrompter := mock_sso.NewMockPrompter(ctrl) + // Set up expected mock calls for _, mp := range tt.mockPrompts { switch mp.method { case "PromptWithDefault": @@ -114,10 +91,6 @@ func TestLoadOrCreateSession(t *testing.T) { mockPrompter.EXPECT(). PromptForRegion(mp.defaultValue). Return(mp.response, mp.err) - case "SelectFromList": - mockPrompter.EXPECT(). - SelectFromList(mp.label, gomock.Any()). - Return(mp.response, mp.err) } } diff --git a/internal/sso/sso.go b/internal/sso/sso.go index 8deb051..2809a66 100644 --- a/internal/sso/sso.go +++ b/internal/sso/sso.go @@ -45,42 +45,30 @@ func (c *RealSSOClient) SetupSSO() error { return fmt.Errorf("failed to select role: %w", err) } - profileName, region, err := c.promptProfileDetails(ssoSession.Region) - if err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return nil - } - return fmt.Errorf("failed to prompt profile details: %w", err) - } + profileName := ssoSession.Name + "-profile" - if err := c.configureAWSProfile(profileName, ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, region); err != nil { + if err := c.configureAWSProfile(profileName, ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { return fmt.Errorf("failed to configure AWS profile: %w", err) } defaultConfigured := profileName == "default" + if !defaultConfigured { - setDefault, err := c.Prompter.PromptYesNo("Set this as the default profile? [Y/n]", true) - if err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return nil - } - return fmt.Errorf("failed to prompt for default profile: %w", err) - } - if setDefault { - if err := c.configureAWSProfile("default", ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, region); err != nil { - return fmt.Errorf("failed to configure AWS default profile: %w", err) - } - defaultConfigured = true + if err := c.configureAWSProfile("default", ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { + return fmt.Errorf("failed to configure AWS default profile: %w", err) } + defaultConfigured = true } printSummary(profileName, ssoSession.Name, ssoSession.StartURL, ssoSession.Region, accountID, role, "", "", "") fmt.Printf("\nSuccessfully configured AWS profile '%s'!\n", profileName) + if defaultConfigured { fmt.Println("You can now use AWS CLI commands without specifying --profile") } else { fmt.Printf("You can now use this profile with AWS CLI commands using: --profile %s\n", profileName) } + return nil } From 002e96688a0bc6f5e936310a51f9cf6c6b876192 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 1 Jul 2025 17:31:28 +0545 Subject: [PATCH 07/51] fix: increased the sso setup time limit from 30 sec to 10 min --- internal/common/services.go | 2 +- internal/sso/client.go | 2 +- utils/common/common.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/common/services.go b/internal/common/services.go index 6a01fb5..d2fb5b4 100644 --- a/internal/common/services.go +++ b/internal/common/services.go @@ -221,7 +221,7 @@ func (s *Services) IsAWSConfigured() bool { return false } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() creds, err := s.Provider.AwsConfig.Credentials.Retrieve(ctx) diff --git a/internal/sso/client.go b/internal/sso/client.go index 112b0ba..a4fa885 100644 --- a/internal/sso/client.go +++ b/internal/sso/client.go @@ -177,7 +177,7 @@ func (c *RealSSOClient) SSOLogin(awsProfile string, refresh, noBrowser bool) err } args = append(args, "--profile", awsProfile) - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() err := c.Executor.RunInteractiveCommand(ctx, "aws", args...) diff --git a/utils/common/common.go b/utils/common/common.go index 1e44f7e..e56234f 100644 --- a/utils/common/common.go +++ b/utils/common/common.go @@ -71,7 +71,7 @@ func terminateProcess(pid int) error { } } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for { select { From 89cc9d0a54b7c52d05f11943e68ed68f44dd1336 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 12:09:43 +0545 Subject: [PATCH 08/51] feat: eks cmd now will prompt for region and profile only --- internal/eks/eks.go | 28 +- internal/eks/eks_test.go | 805 +++++++++------------------------------ 2 files changed, 191 insertions(+), 642 deletions(-) diff --git a/internal/eks/eks.go b/internal/eks/eks.go index cad9808..104dcb0 100644 --- a/internal/eks/eks.go +++ b/internal/eks/eks.go @@ -2,7 +2,6 @@ package eks import ( "context" - "errors" "fmt" "strings" "time" @@ -68,25 +67,7 @@ func NewEKSService( } func (s *EKSService) Run() error { - for { - action, err := s.EPrompter.SelectEKSAction() - if err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return promptUtils.ErrInterrupted - } - return fmt.Errorf("action selection aborted: %v", err) - } - - switch action { - case UpdateKubeConfig: - if err := s.HandleKubeconfigUpdate(); err != nil { - return fmt.Errorf("kubeconfig update failed: %w", err) - } - return nil - case ExitEKS: - return nil - } - } + return s.HandleKubeconfigUpdate() } func (s *EKSService) HandleKubeconfigUpdate() error { @@ -113,14 +94,9 @@ func (s *EKSService) GetEKSClusterDetails() (*models.EKSCluster, string, error) return s.HandleManualCluster() } - confirm, err := s.CPrompter.PromptForConfirmation("Look for EKS clusters in AWS?") - if err != nil || !confirm { - fmt.Println("Proceeding with manual input") - return s.HandleManualCluster() - } - defaultRegion := "" if s.ConnProvider != nil { + var err error defaultRegion, err = s.ConnProvider.GetDefaultRegion() if err != nil { fmt.Printf("Failed to load default region: %v\n", err) diff --git a/internal/eks/eks_test.go b/internal/eks/eks_test.go index 12b9050..f39b8c3 100644 --- a/internal/eks/eks_test.go +++ b/internal/eks/eks_test.go @@ -11,7 +11,6 @@ import ( mock_awsctl "github.com/BerryBytes/awsctl/tests/mock" mock_eks "github.com/BerryBytes/awsctl/tests/mock/eks" "github.com/BerryBytes/awsctl/utils/common" - promptUtils "github.com/BerryBytes/awsctl/utils/prompt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -32,24 +31,7 @@ func TestNewEKSService(t *testing.T) { assert.NotNil(t, service.FileSystem) } -func TestEKSService_Run_ExitAction(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - SelectEKSAction(). - Return(eks.ExitEKS, nil) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - } - - err := service.Run() - assert.NoError(t, err) -} - -func TestEKSService_Run_UpdateKubeConfig(t *testing.T) { +func TestEKSService_Run_Success(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -62,9 +44,6 @@ func TestEKSService_Run_UpdateKubeConfig(t *testing.T) { testRegion := "us-west-2" mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - SelectEKSAction(). - Return(eks.UpdateKubeConfig, nil) mockEPrompter.EXPECT(). PromptForProfile(). Return(testProfile, nil) @@ -86,9 +65,6 @@ func TestEKSService_Run_UpdateKubeConfig(t *testing.T) { Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) @@ -122,157 +98,7 @@ func TestEKSService_Run_UpdateKubeConfig(t *testing.T) { assert.NoError(t, err) } -func TestEKSService_Run_Interrupted(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - SelectEKSAction(). - Return(eks.ExitEKS, promptUtils.ErrInterrupted) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - } - - err := service.Run() - assert.Equal(t, promptUtils.ErrInterrupted, err) -} - -func TestEKSService_getEKSClusterDetails_ManualFallback(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return("test-cluster", "https://test.endpoint", "ca-data", "us-west-2", nil) - - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(false) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - ConnServices: mockConnServices, - } - - cluster, profile, err := service.GetEKSClusterDetails() - assert.NoError(t, err) - assert.Equal(t, "test-cluster", cluster.ClusterName) - assert.Equal(t, "", profile) -} - -func TestEKSService_handleManualCluster_Success(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return("test-cluster", "https://test.endpoint", "ca-data", "us-west-2", nil) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - } - - cluster, profile, err := service.HandleManualCluster() - assert.NoError(t, err) - assert.Equal(t, "test-cluster", cluster.ClusterName) - assert.Equal(t, "https://test.endpoint", cluster.Endpoint) - assert.Equal(t, "us-west-2", cluster.Region) - assert.Equal(t, "", profile) -} - -func TestEKSService_handleManualCluster_Error(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return("", "", "", "", errors.New("input error")) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - } - - _, _, err := service.HandleManualCluster() - assert.Error(t, err) -} - -func TestEKSService_HandleKubeconfigUpdate_Success(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - testCluster := models.EKSCluster{ - ClusterName: "test-cluster", - Endpoint: "https://test.endpoint", - Region: "us-west-2", - } - testProfile := "default" - testRegion := "us-west-2" - - mockEKSClient := mock_eks.NewMockEKSAdapterInterface(ctrl) - mockEKSClient.EXPECT(). - ListEKSClusters(gomock.Any()). - Return([]models.EKSCluster{testCluster}, nil) - mockEKSClient.EXPECT(). - UpdateKubeconfig(&testCluster, testProfile). - Return(nil) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - mockFactory.EXPECT(). - NewEKSClient(gomock.Any(), gomock.Any()). - Return(mockEKSClient) - - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - mockConfigLoader.EXPECT(). - LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{ - Region: testRegion, - Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{AccessKeyID: "test"}, nil - }), - }, nil) - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForProfile(). - Return(testProfile, nil) - mockEPrompter.EXPECT(). - PromptForEKSCluster([]models.EKSCluster{testCluster}). - Return(testCluster.ClusterName, nil) - - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) - mockCPrompter.EXPECT(). - PromptForRegion(""). - Return(testRegion, nil) - - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, - } - - err := service.HandleKubeconfigUpdate() - assert.NoError(t, err) -} - -func TestEKSService_HandleKubeconfigUpdate_Error(t *testing.T) { +func TestEKSService_Run_AWSNotConfigured(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -280,355 +106,70 @@ func TestEKSService_HandleKubeconfigUpdate_Error(t *testing.T) { defer func() { os.Stdout = old }() os.Stdout = os.NewFile(0, os.DevNull) - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) + clusterName := "test-cluster" + endpoint := "https://test.endpoint" + region := "us-west-2" + caData := "ca-data" - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) - mockCPrompter.EXPECT(). - PromptForRegion(""). - Return("us-west-2", nil) - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForProfile(). - Return("", errors.New("profile error")) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return("", "", "", "", errors.New("manual input error")) - - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - FileSystem: &common.RealFileSystem{}, + expectedCluster := &models.EKSCluster{ + ClusterName: clusterName, + Endpoint: endpoint, + Region: region, + CertificateAuthorityData: caData, } - err := service.HandleKubeconfigUpdate() - assert.Error(t, err) - assert.Contains(t, err.Error(), "manual input error") -} - -func TestEKSService_getEKSClusterDetails_AWSConfigured(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - - cluster := models.EKSCluster{ClusterName: "test-cluster"} - testRegion := "us-west-2" - testProfile := "default" - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). - PromptForProfile(). - Return(testProfile, nil) - mockEPrompter.EXPECT(). - PromptForEKSCluster([]models.EKSCluster{cluster}). - Return(cluster.ClusterName, nil) + PromptForManualCluster(). + Return(clusterName, endpoint, caData, region, nil) mockEKSClient := mock_eks.NewMockEKSAdapterInterface(ctrl) mockEKSClient.EXPECT(). - ListEKSClusters(gomock.Any()). - Return([]models.EKSCluster{cluster}, nil) - - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) - - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) - mockCPrompter.EXPECT(). - PromptForRegion(""). - Return(testRegion, nil) - - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - mockConfigLoader.EXPECT(). - LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{ - Region: testRegion, - Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{AccessKeyID: "test"}, nil - }), - }, nil) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - mockFactory.EXPECT(). - NewEKSClient(gomock.Any(), gomock.Any()). - Return(mockEKSClient) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, - } - - resultCluster, profile, err := service.GetEKSClusterDetails() - assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, resultCluster.ClusterName) - assert.Equal(t, testProfile, profile) -} -func TestEKSService_getEKSClusterDetails_CredentialsRetrieveError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - - cluster := models.EKSCluster{ClusterName: "test-cluster"} - testRegion := "us-west-2" - testProfile := "default" - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForProfile(). - Return(testProfile, nil) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) - - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) - - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) - mockCPrompter.EXPECT(). - PromptForRegion(""). - Return(testRegion, nil) - - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - mockConfigLoader.EXPECT(). - LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{ - Region: testRegion, - Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{}, errors.New("credentials error") - }), - }, nil) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, - } - - clusterResult, profile, err := service.GetEKSClusterDetails() - assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) - assert.Equal(t, "", profile) -} - -func TestRealConfigLoader(t *testing.T) { - loader := &eks.RealConfigLoader{} - cfg, err := loader.LoadDefaultConfig(context.TODO()) - assert.NoError(t, err) - assert.NotNil(t, cfg) -} - -func TestRealEKSClientFactory(t *testing.T) { - factory := &eks.RealEKSClientFactory{} - cfg := aws.Config{Region: "us-west-2"} - client := factory.NewEKSClient(cfg, &common.RealFileSystem{}) - assert.NotNil(t, client) -} - -func TestEKSService_Run_ActionSelectionError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - SelectEKSAction(). - Return(eks.EKSAction(99), errors.New("selection error")) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - } - - err := service.Run() - assert.Error(t, err) - assert.Contains(t, err.Error(), "action selection aborted") -} - -func TestEKSService_Run_UpdateKubeConfigError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - SelectEKSAction(). - Return(eks.UpdateKubeConfig, nil) + UpdateKubeconfig(expectedCluster, ""). + Return(nil) mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) mockConnServices.EXPECT(). IsAWSConfigured(). Return(false) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return("", "", "", "", errors.New("manual input error")) - service := &eks.EKSService{ EPrompter: mockEPrompter, ConnServices: mockConnServices, + EKSClient: mockEKSClient, + FileSystem: &common.RealFileSystem{}, } err := service.Run() - assert.Error(t, err) - assert.Contains(t, err.Error(), "kubeconfig update failed") -} - -func TestEKSService_isAWSConfigured(t *testing.T) { - t.Run("nil ConnServices", func(t *testing.T) { - service := &eks.EKSService{} - assert.False(t, service.IsAWSConfigured()) - }) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - t.Run("configured", func(t *testing.T) { - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) - - service := &eks.EKSService{ - ConnServices: mockConnServices, - } - assert.True(t, service.IsAWSConfigured()) - }) - - t.Run("not configured", func(t *testing.T) { - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(false) - - service := &eks.EKSService{ - ConnServices: mockConnServices, - } - assert.False(t, service.IsAWSConfigured()) - }) -} - -func TestEKSService_getEKSClusterDetails_PromptForConfirmationFailure(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - - cluster := models.EKSCluster{ClusterName: "test-cluster"} - - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) - mockEPrompter.EXPECT(). - PromptForManualCluster(). - Return(cluster.ClusterName, "https://test.endpoint", "ca-data", "us-west-2", nil) - - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) - mockConnServices.EXPECT(). - IsAWSConfigured(). - Return(true) - - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - AnyTimes(). - DoAndReturn(func(_ string) (bool, error) { - return false, errors.New("confirmation error") - }) - - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mock_eks.NewMockConfigLoader(ctrl), - EKSClientFactory: mock_eks.NewMockEKSClientFactory(ctrl), - FileSystem: &common.RealFileSystem{}, - } - - clusterResult, profile, err := service.GetEKSClusterDetails() assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) - assert.Equal(t, "", profile) } -func TestEKSService_getEKSClusterDetails_PromptForRegionError(t *testing.T) { +func TestEKSService_Run_ManualClusterError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - - cluster := models.EKSCluster{ClusterName: "test-cluster"} - mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). PromptForManualCluster(). - Return(cluster.ClusterName, "https://test.endpoint", "ca-data", "us-west-2", nil) + Return("", "", "", "", errors.New("kubeconfig update failed")) mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) mockConnServices.EXPECT(). IsAWSConfigured(). - Return(true) - - mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) - mockCPrompter.EXPECT(). - PromptForRegion(""). - Return("", errors.New("region error")) + Return(false) service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mock_eks.NewMockConfigLoader(ctrl), - EKSClientFactory: mock_eks.NewMockEKSClientFactory(ctrl), - FileSystem: &common.RealFileSystem{}, + EPrompter: mockEPrompter, + ConnServices: mockConnServices, + FileSystem: &common.RealFileSystem{}, } - clusterResult, profile, err := service.GetEKSClusterDetails() - assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) - assert.Equal(t, "", profile) + err := service.Run() + assert.Error(t, err) + assert.Contains(t, err.Error(), "kubeconfig update failed") } -func TestEKSService_getEKSClusterDetails_ConnProviderDefaultRegionError(t *testing.T) { +func TestEKSService_GetEKSClusterDetails_AWSConfigured(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -659,9 +200,6 @@ func TestEKSService_getEKSClusterDetails_ConnProviderDefaultRegionError(t *testi Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) @@ -687,7 +225,6 @@ func TestEKSService_getEKSClusterDetails_ConnProviderDefaultRegionError(t *testi ConnServices: mockConnServices, ConfigLoader: mockConfigLoader, EKSClientFactory: mockFactory, - EKSClient: nil, FileSystem: &common.RealFileSystem{}, } @@ -697,22 +234,43 @@ func TestEKSService_getEKSClusterDetails_ConnProviderDefaultRegionError(t *testi assert.Equal(t, testProfile, profile) } -func TestEKSService_getEKSClusterDetails_ConfigLoadSSOError(t *testing.T) { +func TestEKSService_GetEKSClusterDetails_ManualFallback(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - cluster := models.EKSCluster{ClusterName: "test-cluster"} testRegion := "us-west-2" - testProfile := "default" mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). - PromptForProfile(). - Return(testProfile, nil) + PromptForManualCluster(). + Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) + + mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) + mockConnServices.EXPECT(). + IsAWSConfigured(). + Return(false) + + service := &eks.EKSService{ + EPrompter: mockEPrompter, + ConnServices: mockConnServices, + FileSystem: &common.RealFileSystem{}, + } + + resultCluster, profile, err := service.GetEKSClusterDetails() + assert.NoError(t, err) + assert.Equal(t, cluster.ClusterName, resultCluster.ClusterName) + assert.Equal(t, "", profile) +} + +func TestEKSService_GetEKSClusterDetails_RegionPromptError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cluster := models.EKSCluster{ClusterName: "test-cluster"} + testRegion := "us-west-2" + + mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). PromptForManualCluster(). Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) @@ -723,44 +281,27 @@ func TestEKSService_getEKSClusterDetails_ConfigLoadSSOError(t *testing.T) { Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). - Return(testRegion, nil) - - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - mockConfigLoader.EXPECT(). - LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{}, errors.New("SSO session expired")) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) + Return("", errors.New("region error")) service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, + EPrompter: mockEPrompter, + CPrompter: mockCPrompter, + ConnServices: mockConnServices, + FileSystem: &common.RealFileSystem{}, } - clusterResult, profile, err := service.GetEKSClusterDetails() + resultCluster, profile, err := service.GetEKSClusterDetails() assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) + assert.Equal(t, cluster.ClusterName, resultCluster.ClusterName) assert.Equal(t, "", profile) } -func TestEKSService_getEKSClusterDetails_ConfigLoadGenericError(t *testing.T) { +func TestEKSService_GetEKSClusterDetails_ConfigLoadError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - old := os.Stdout - defer func() { os.Stdout = old }() - os.Stdout = os.NewFile(0, os.DevNull) - cluster := models.EKSCluster{ClusterName: "test-cluster"} testRegion := "us-west-2" testProfile := "default" @@ -779,9 +320,6 @@ func TestEKSService_getEKSClusterDetails_ConfigLoadGenericError(t *testing.T) { Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) @@ -789,27 +327,115 @@ func TestEKSService_getEKSClusterDetails_ConfigLoadGenericError(t *testing.T) { mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) mockConfigLoader.EXPECT(). LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{}, errors.New("invalid configuration")) + Return(aws.Config{}, errors.New("config error")) - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) + service := &eks.EKSService{ + EPrompter: mockEPrompter, + CPrompter: mockCPrompter, + ConnServices: mockConnServices, + ConfigLoader: mockConfigLoader, + FileSystem: &common.RealFileSystem{}, + } + + resultCluster, profile, err := service.GetEKSClusterDetails() + assert.NoError(t, err) + assert.Equal(t, cluster.ClusterName, resultCluster.ClusterName) + assert.Equal(t, "", profile) +} + +func TestEKSService_HandleManualCluster_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cluster := models.EKSCluster{ + ClusterName: "test-cluster", + Endpoint: "https://test.endpoint", + Region: "us-west-2", + } + + mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) + mockEPrompter.EXPECT(). + PromptForManualCluster(). + Return(cluster.ClusterName, cluster.Endpoint, "ca-data", cluster.Region, nil) service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, + EPrompter: mockEPrompter, } - clusterResult, profile, err := service.GetEKSClusterDetails() + resultCluster, profile, err := service.HandleManualCluster() assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) + assert.Equal(t, cluster.ClusterName, resultCluster.ClusterName) + assert.Equal(t, cluster.Endpoint, resultCluster.Endpoint) + assert.Equal(t, cluster.Region, resultCluster.Region) assert.Equal(t, "", profile) } -func TestEKSService_getEKSClusterDetails_ListClustersError(t *testing.T) { +func TestEKSService_HandleManualCluster_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) + mockEPrompter.EXPECT(). + PromptForManualCluster(). + Return("", "", "", "", errors.New("input error")) + + service := &eks.EKSService{ + EPrompter: mockEPrompter, + } + + _, _, err := service.HandleManualCluster() + assert.Error(t, err) +} + +func TestEKSService_IsAWSConfigured(t *testing.T) { + t.Run("nil ConnServices", func(t *testing.T) { + service := &eks.EKSService{} + assert.False(t, service.IsAWSConfigured()) + }) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("configured", func(t *testing.T) { + mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) + mockConnServices.EXPECT(). + IsAWSConfigured(). + Return(true) + + service := &eks.EKSService{ + ConnServices: mockConnServices, + } + assert.True(t, service.IsAWSConfigured()) + }) + + t.Run("not configured", func(t *testing.T) { + mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) + mockConnServices.EXPECT(). + IsAWSConfigured(). + Return(false) + + service := &eks.EKSService{ + ConnServices: mockConnServices, + } + assert.False(t, service.IsAWSConfigured()) + }) +} + +func TestRealConfigLoader(t *testing.T) { + loader := &eks.RealConfigLoader{} + cfg, err := loader.LoadDefaultConfig(context.TODO()) + assert.NoError(t, err) + assert.NotNil(t, cfg) +} + +func TestRealEKSClientFactory(t *testing.T) { + factory := &eks.RealEKSClientFactory{} + cfg := aws.Config{Region: "us-west-2"} + client := factory.NewEKSClient(cfg, &common.RealFileSystem{}) + assert.NotNil(t, client) +} + +func TestEKSService_GetEKSClusterDetails_ProfileError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -817,68 +443,39 @@ func TestEKSService_getEKSClusterDetails_ListClustersError(t *testing.T) { defer func() { os.Stdout = old }() os.Stdout = os.NewFile(0, os.DevNull) - cluster := models.EKSCluster{ClusterName: "test-cluster"} testRegion := "us-west-2" - testProfile := "default" + cluster := models.EKSCluster{ClusterName: "test-cluster"} mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). PromptForProfile(). - Return(testProfile, nil) + Return("", errors.New("profile error")) mockEPrompter.EXPECT(). PromptForManualCluster(). Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) - mockEKSClient := mock_eks.NewMockEKSAdapterInterface(ctrl) - mockEKSClient.EXPECT(). - ListEKSClusters(gomock.Any()). - Return(nil, errors.New("list clusters error")) - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) mockConnServices.EXPECT(). IsAWSConfigured(). Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) - mockConfigLoader := mock_eks.NewMockConfigLoader(ctrl) - mockConfigLoader.EXPECT(). - LoadDefaultConfig(gomock.Any(), gomock.Any()). - Return(aws.Config{ - Region: testRegion, - Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{AccessKeyID: "test"}, nil - }), - }, nil) - - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - mockFactory.EXPECT(). - NewEKSClient(gomock.Any(), gomock.Any()). - Return(mockEKSClient) - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, + EPrompter: mockEPrompter, + CPrompter: mockCPrompter, + ConnServices: mockConnServices, + FileSystem: &common.RealFileSystem{}, } - clusterResult, profile, err := service.GetEKSClusterDetails() + _, _, err := service.GetEKSClusterDetails() assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) - assert.Equal(t, "", profile) } -func TestEKSService_getEKSClusterDetails_EmptyClusterList(t *testing.T) { +func TestEKSService_GetEKSClusterDetails_CredentialsError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -886,9 +483,9 @@ func TestEKSService_getEKSClusterDetails_EmptyClusterList(t *testing.T) { defer func() { os.Stdout = old }() os.Stdout = os.NewFile(0, os.DevNull) - cluster := models.EKSCluster{ClusterName: "test-cluster"} - testRegion := "us-west-2" testProfile := "default" + testRegion := "us-west-2" + cluster := models.EKSCluster{ClusterName: "test-cluster"} mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). @@ -898,20 +495,12 @@ func TestEKSService_getEKSClusterDetails_EmptyClusterList(t *testing.T) { PromptForManualCluster(). Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) - mockEKSClient := mock_eks.NewMockEKSAdapterInterface(ctrl) - mockEKSClient.EXPECT(). - ListEKSClusters(gomock.Any()). - Return([]models.EKSCluster{}, nil) - mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) mockConnServices.EXPECT(). IsAWSConfigured(). Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) @@ -922,32 +511,23 @@ func TestEKSService_getEKSClusterDetails_EmptyClusterList(t *testing.T) { Return(aws.Config{ Region: testRegion, Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{AccessKeyID: "test"}, nil + return aws.Credentials{}, errors.New("SSO token expired") }), }, nil) - mockFactory := mock_eks.NewMockEKSClientFactory(ctrl) - mockFactory.EXPECT(). - NewEKSClient(gomock.Any(), gomock.Any()). - Return(mockEKSClient) - service := &eks.EKSService{ - EPrompter: mockEPrompter, - CPrompter: mockCPrompter, - ConnServices: mockConnServices, - ConfigLoader: mockConfigLoader, - EKSClientFactory: mockFactory, - EKSClient: nil, - FileSystem: &common.RealFileSystem{}, + EPrompter: mockEPrompter, + CPrompter: mockCPrompter, + ConnServices: mockConnServices, + ConfigLoader: mockConfigLoader, + FileSystem: &common.RealFileSystem{}, } - clusterResult, profile, err := service.GetEKSClusterDetails() + _, _, err := service.GetEKSClusterDetails() assert.NoError(t, err) - assert.Equal(t, cluster.ClusterName, clusterResult.ClusterName) - assert.Equal(t, "", profile) } -func TestEKSService_getEKSClusterDetails_ClusterNotFound(t *testing.T) { +func TestEKSService_GetEKSClusterDetails_NoClustersFound(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -955,22 +535,22 @@ func TestEKSService_getEKSClusterDetails_ClusterNotFound(t *testing.T) { defer func() { os.Stdout = old }() os.Stdout = os.NewFile(0, os.DevNull) - cluster := models.EKSCluster{ClusterName: "test-cluster"} - testRegion := "us-west-2" testProfile := "default" + testRegion := "us-west-2" + cluster := models.EKSCluster{ClusterName: "test-cluster"} mockEPrompter := mock_eks.NewMockEKSPromptInterface(ctrl) mockEPrompter.EXPECT(). PromptForProfile(). Return(testProfile, nil) mockEPrompter.EXPECT(). - PromptForEKSCluster([]models.EKSCluster{cluster}). - Return("non-existent-cluster", nil) + PromptForManualCluster(). + Return(cluster.ClusterName, "https://test.endpoint", "ca-data", testRegion, nil) mockEKSClient := mock_eks.NewMockEKSAdapterInterface(ctrl) mockEKSClient.EXPECT(). ListEKSClusters(gomock.Any()). - Return([]models.EKSCluster{cluster}, nil) + Return([]models.EKSCluster{}, nil) mockConnServices := mock_awsctl.NewMockServicesInterface(ctrl) mockConnServices.EXPECT(). @@ -978,9 +558,6 @@ func TestEKSService_getEKSClusterDetails_ClusterNotFound(t *testing.T) { Return(true) mockCPrompter := mock_awsctl.NewMockConnectionPrompter(ctrl) - mockCPrompter.EXPECT(). - PromptForConfirmation("Look for EKS clusters in AWS?"). - Return(true, nil) mockCPrompter.EXPECT(). PromptForRegion(""). Return(testRegion, nil) @@ -1006,13 +583,9 @@ func TestEKSService_getEKSClusterDetails_ClusterNotFound(t *testing.T) { ConnServices: mockConnServices, ConfigLoader: mockConfigLoader, EKSClientFactory: mockFactory, - EKSClient: nil, FileSystem: &common.RealFileSystem{}, } - clusterResult, profile, err := service.GetEKSClusterDetails() - assert.Error(t, err) - assert.Contains(t, err.Error(), "selected cluster not found") - assert.Nil(t, clusterResult) - assert.Equal(t, "", profile) + _, _, err := service.GetEKSClusterDetails() + assert.NoError(t, err) } From c9b909aeabb6410473d40ec744d215e9722d8ae2 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 16:14:44 +0545 Subject: [PATCH 09/51] chore: edited the script to use dynamic version for local installation --- install-awsctl.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install-awsctl.sh b/install-awsctl.sh index 9e40667..6ba1c01 100755 --- a/install-awsctl.sh +++ b/install-awsctl.sh @@ -7,8 +7,9 @@ BINARY_NAME="awsctl" BUILD_DIR="$(pwd)/bin" INSTALL_DIR="$HOME/awsctl" GC_FLAGS="all=-N -l" -LD_FLAGS="-s -w" +VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.2.0") +LD_FLAGS="-X 'main.Version=$VERSION' -s -w" # Detect shell detect_shell() { case "$SHELL" in From 798df29b3fdaa449dbee8e8485f885f0e60dfcb5 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 16:15:50 +0545 Subject: [PATCH 10/51] chore: next release [release] From 57b20f63f7295b944a3d35c3736cdfc38302817a Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 2 Jul 2025 10:38:39 +0000 Subject: [PATCH 11/51] Update CHANGELOG.md for v0.4.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347ab7c..9c3bba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# awsctl - v0.4.0 + +## Changes since v0.3.0 + +- [c9b909a](https://github.com/BerryBytes/awsctl/commit/c9b909aeabb6410473d40ec744d215e9722d8ae2) Edited the script to use dynamic version for local installation (sawnjordan ) +- [89cc9d0](https://github.com/BerryBytes/awsctl/commit/89cc9d0a54b7c52d05f11943e68ed68f44dd1336) Eks cmd now will prompt for region and profile only (sawnjordan ) +- [002e966](https://github.com/BerryBytes/awsctl/commit/002e96688a0bc6f5e936310a51f9cf6c6b876192) Increased the sso setup time limit from 30 sec to 10 min (sawnjordan ) +- [fe9001d](https://github.com/BerryBytes/awsctl/commit/fe9001d2660783fc81b7c92b422365463a7c1364) On sso setup, removed extra prompt and setup via start url and region (sawnjordan ) +- [0306d0e](https://github.com/BerryBytes/awsctl/commit/0306d0ed33239873aa626b0d624b9fa4f913e0ff) Increased the sso login timelimit to 10 minutes from 30 sec (sawnjordan ) + +Generated on 2025-07-02T10:38:37Z + # awsctl - v0.3.0 ## Changes since v0.2.0 From 3beacfa459974ab856fa0daf0081190d17765068 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 17:21:22 +0545 Subject: [PATCH 12/51] test: beta release test workflow [release] --- .github/workflows/releaser-internal.yaml | 66 ++++++++++++++++++++++++ .github/workflows/releaser.yaml | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/releaser-internal.yaml diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml new file mode 100644 index 0000000..b2a6a37 --- /dev/null +++ b/.github/workflows/releaser-internal.yaml @@ -0,0 +1,66 @@ +name: Beta Release Test + +on: + push: + branches: + - release/sanjog/int-release + paths-ignore: + - "**.md" + workflow_dispatch: + +concurrency: + group: release-test-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + release: + if: "contains(github.event.head_commit.message, '[release]') || github.event_name == 'workflow_dispatch'" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + cache: true + + - name: Install dependencies + run: | + npm install --global semver + + - name: Get current tag + id: tags + run: | + CURRENT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT + + - name: Determine new version + id: new_version + run: | + CURRENT_TAG=${{ steps.tags.outputs.current_tag }} + if [ -z "$CURRENT_TAG" ]; then + echo "new_tag=v0.1.0-TEST" >> $GITHUB_OUTPUT # TEST suffix instead of beta-internal + exit 0 + fi + + VERSION=$(echo "$CURRENT_TAG" | sed 's/^v//' | sed 's/-TEST//') + NEW_VERSION=$(semver "$VERSION" -i patch) + NEW_TAG="v${NEW_VERSION}-TEST" # TEST suffix for identification + echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + + - name: Run GoReleaser (Dry Run) + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: v2.9.0 + args: release --clean --skip-publish --snapshot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index 81939f2..4f7f5a7 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -4,7 +4,7 @@ on: push: branches: - main - - develop + # - develop workflow_dispatch: concurrency: From 24121ef5c7f0f619363df05b0b1bfd678639add0 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 17:26:18 +0545 Subject: [PATCH 13/51] test: test beta release [release] --- .github/workflows/releaser-internal.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index b2a6a37..b681441 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -47,7 +47,7 @@ jobs: run: | CURRENT_TAG=${{ steps.tags.outputs.current_tag }} if [ -z "$CURRENT_TAG" ]; then - echo "new_tag=v0.1.0-TEST" >> $GITHUB_OUTPUT # TEST suffix instead of beta-internal + echo "new_tag=v0.1.0-TEST" >> $GITHUB_OUTPUT exit 0 fi @@ -61,6 +61,6 @@ jobs: with: distribution: goreleaser version: v2.9.0 - args: release --clean --skip-publish --snapshot + args: release --clean --snapshot env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e7223dabd232019ffe7ff0da34bd80774548c4ad Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 17:43:31 +0545 Subject: [PATCH 14/51] test: test beta release workflow on develop branch --- .github/workflows/releaser-internal.yaml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index b681441..a49fdbc 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -1,15 +1,15 @@ -name: Beta Release Test +name: Beta Release on Develop on: push: branches: - - release/sanjog/int-release + - develop paths-ignore: - "**.md" workflow_dispatch: concurrency: - group: release-test-${{ github.ref }} + group: release-develop cancel-in-progress: true permissions: @@ -47,20 +47,21 @@ jobs: run: | CURRENT_TAG=${{ steps.tags.outputs.current_tag }} if [ -z "$CURRENT_TAG" ]; then - echo "new_tag=v0.1.0-TEST" >> $GITHUB_OUTPUT + echo "First release - creating v0.1.0-beta-internal" + echo "new_tag=v0.1.0-beta-internal" >> $GITHUB_OUTPUT exit 0 fi - VERSION=$(echo "$CURRENT_TAG" | sed 's/^v//' | sed 's/-TEST//') + VERSION=$(echo "$CURRENT_TAG" | sed 's/^v//' | sed 's/-beta-internal//') NEW_VERSION=$(semver "$VERSION" -i patch) - NEW_TAG="v${NEW_VERSION}-TEST" # TEST suffix for identification + NEW_TAG="v${NEW_VERSION}-beta-internal" echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT - - name: Run GoReleaser (Dry Run) + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: v2.9.0 - args: release --clean --snapshot + args: release --clean --prerelease env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 56ad6634f18a5363a0e1ac9fbba149cd7d47b81b Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 2 Jul 2025 17:44:17 +0545 Subject: [PATCH 15/51] test: beta release [release] From 7d8f804dd4e3ab474ff8c109833e6a06c0ef42e7 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Thu, 3 Jul 2025 09:28:17 +0545 Subject: [PATCH 16/51] chore: test develop beta release [release] --- .github/workflows/releaser-internal.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index a49fdbc..059b96d 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -62,6 +62,6 @@ jobs: with: distribution: goreleaser version: v2.9.0 - args: release --clean --prerelease + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9691d40ee5607000cd43758637bf0467f042343b Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Thu, 3 Jul 2025 09:47:13 +0545 Subject: [PATCH 17/51] docs: updated readme with the changes made on sso setup and eks command [release] --- README.md | 29 +++++++++++++++-------------- docs/usage/commands.md | 36 +++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6c0c9e4..5637132 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [Usage](#usage) - [Commands](#commands) - [Contributing](#contributing) -- [Code Review Automation](#code-review-automation) +- [Code Review Automation](#code-review-automation) - [Releasing](#releasing) - [Credits](#credits) - [License](#license) @@ -97,7 +97,7 @@ chmod +x install-awsctl.sh 3. Run the startup script: - #### First Time Installation -If this is your first time installing, use the `source` command: + If this is your first time installing, use the `source` command: ```bash source ./install-awsctl.sh @@ -106,11 +106,13 @@ source ./install-awsctl.sh This ensures environment changes (like `PATH` updates) take effect immediately. ### Why use `source`? + - Executes in current shell session - Updates environment variables immediately - No terminal restart required ### For Updates + Run normally: ```bash @@ -136,26 +138,25 @@ ssoSessions: startUrl: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" region: "XX-XXXX-X" scopes: "sso:account:access" - - ``` + **Note**: `scopes` can be empty. Default value will be `sso:account:access` + ### Commands The following table summarizes the available `awsctl` commands: -| Command | Description | -|--------------------|---------------------------------------------------------------------------------------------------| -| `awsctl sso setup` | Creates or updates an AWS SSO profile. Uses config file in `~/.config/awsctl/` if available, or prompts for required details. Optionally sets it as the default and authenticates. | -| `awsctl sso init` | Starts SSO authentication by allowing you to select from existing AWS SSO profiles (created via `awsctl sso setup`). Useful for switching between multiple configured SSO profiles. | -| `awsctl bastion` | Manages SSH/SSM connections, SOCKS proxy, or port forwarding to bastion hosts or EC2 instances. | -| `awsctl rds` | Connects to RDS databases directly or via SSH/SSM tunnels. | -| `awsctl eks` | Updates kubeconfig for accessing Amazon EKS clusters. | -| `awsctl ecr` | Authenticates to Amazon ECR for container image operations. | +| Command | Description | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `awsctl sso setup` | Creates or updates an AWS SSO profile. If a config file is available at `~/.config/awsctl/`, it will be used; otherwise, you will be prompted to enter the SSO Start URL and Region. The selected profile is then set as the default and authenticated. | +| `awsctl sso init` | Starts SSO authentication by allowing you to select from existing AWS SSO profiles (created via `awsctl sso setup`). Useful for switching between multiple configured SSO profiles. | +| `awsctl bastion` | Manages SSH/SSM connections, SOCKS proxy, or port forwarding to bastion hosts or EC2 instances. | +| `awsctl rds` | Connects to RDS databases directly or via SSH/SSM tunnels. | +| `awsctl eks` | Updates kubeconfig for accessing Amazon EKS clusters. | +| `awsctl ecr` | Authenticates to Amazon ECR for container image operations. | #### For detailed CLI command usage, see [Command Usage Documentation](docs/usage/commands.md). - ### Contributing We welcome contributions! Please see our [contributing guidelines](CONTRIBUTING.md) for more details. @@ -167,13 +168,13 @@ This project uses [Coderabbit AI](https://www.coderabbit.ai/) to assist with pul **Role**: Automatically reviews pull requests for code quality, potential bugs, best practices, and documentation gaps. **How it works**: + - Summarizes PR changes. - Provides line-by-line suggestions. - Offers codebase-wide analysis. **Note**: Suggestions by Coderabbit are recommendations. Final review decisions are made by maintainers. - ### Releasing To trigger a release, push a commit to `main` with `[release]` in the commit message (e.g., `git commit -m "Add feature [release]"`). The workflow will auto-increment the version, tag it, and create a draft release. diff --git a/docs/usage/commands.md b/docs/usage/commands.md index 811ec51..08d651b 100644 --- a/docs/usage/commands.md +++ b/docs/usage/commands.md @@ -3,19 +3,19 @@ ## Commands ### `awsctl sso setup` + Creates or updates AWS SSO profiles. - Prompts for: - SSO Start URL - AWS Region - - Scopes (default: `sso:account:access`) - - Option to set as default profile - Uses defaults from `~/.config/awsctl/config.yml` if available. -- Authenticates the profile immediately after setup. +- Automatically sets the profile as default and authenticates it after setup. --- ### `awsctl sso init` + Starts SSO authentication using one of the configured SSO profiles. - Selects from available profiles created via `awsctl sso setup` @@ -25,10 +25,13 @@ Starts SSO authentication using one of the configured SSO profiles. --- ### `awsctl bastion` + Manages connections to bastion hosts via SSH, SSM, or tunnels. #### Instance Detection + - If SSO is configured, prompts: + - "Look for bastion hosts in AWS?" - If yes, searches for EC2 instances with the name or tags containing `bastion` for the **selected profile**. - Allows easier selection from discovered instances. @@ -38,6 +41,7 @@ Manages connections to bastion hosts via SSH, SSM, or tunnels. - Allows manual entry of bastion host, SSH username, and SSH key. #### Connection Options + 1. SSH: - Public or Private IP (uses EC2 Instance Connect if needed). 2. SSM: @@ -47,6 +51,7 @@ Manages connections to bastion hosts via SSH, SSM, or tunnels. #### Requirements for SSM and EC2 Instance Connect **1. SSM (AWS Systems Manager) Requirements** + - **IAM Role Attached to Instance**: - Must have the following AWS managed policies (or equivalent custom policies): - `AmazonSSMManagedInstanceCore` @@ -60,19 +65,21 @@ Manages connections to bastion hosts via SSH, SSM, or tunnels. - Ensure the **SSM Agent** is installed and running on the EC2 instance. **2. EC2 Instance Connect Requirements** + - **IAM Permissions for Caller/User**: - `ec2-instance-connect:SendSSHPublicKey` - `ec2:DescribeInstances` - `ec2:GetConsoleOutput` (optional) - **Public DNS/IP Access**: + - The instance **must have a public IPv4 address or public DNS**, unless used via a bastion or SSM tunnel. - **VPC Endpoint (Required if the instance is in a private subnet without internet access)**: - Create an **Interface VPC Endpoint** for `com.amazonaws..ec2-instance-connect` - Required if the instance is in a private subnet without internet access, allowing EC2 Instance Connect API calls to AWS securely. - #### Extras + - **SOCKS5 Proxy**: - Prompts for: - SOCKS proxy port (default: `1080`) @@ -88,24 +95,30 @@ Manages connections to bastion hosts via SSH, SSM, or tunnels. --- ### `awsctl rds` + Connects to RDS databases with flexibility. #### Supported Modes: + - **Direct Connect**: If the RDS instance is publicly accessible. - **Via Bastion**: SSH or SSM tunnel through a bastion host. #### Supported Databases: + - PostgreSQL, MySQL, others (depending on configuration). - Dynamic port assignment to avoid collisions. #### Authentication Methods + - **Token** (IAM Database Authentication) - **Native Password** (Database user password) ##### Native Password + - Use the **initial password** defined when creating the RDS instance or the password configured for that database user. ##### Token (IAM Authentication) + - Requires **IAM database authentication** to be enabled on the RDS instance. - **For MySQL**: - Users must be configured with the `AWSAuthenticationPlugin`. @@ -114,29 +127,37 @@ Connects to RDS databases with flexibility. - You can either **create a new IAM-auth-enabled database user** or **alter existing users** to support IAM-based login. ###### Example: Enable IAM Authentication for Database Users + **MySQL:** + ```sql CREATE USER 'dbuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'; GRANT ALL PRIVILEGES ON database_name.* TO 'dbuser'@'%'; ``` **PostgreSQL:** + ```sql CREATE USER dbuser WITH LOGIN; GRANT rds_iam TO dbuser; ``` + --- ### `awsctl eks` + Simplifies access to Amazon EKS clusters. -- Features: - - Lists available EKS clusters for the AWS profile/region. - - Updates or generates `~/.kube/config` with the selected cluster’s credentials. +- Prompts for: + - **AWS Region** + - **SSO Profile** (fetched from `~/.aws/config`; must be set up via `awsctl sso setup`) +- Lists available EKS clusters for the selected region and profile. +- Prompts you to select a cluster and updates (or creates) your `~/.kube/config` with the cluster’s credentials. --- ### `awsctl ecr` + Handles authentication to Amazon ECR for Docker or container image workflows. - Features: @@ -155,3 +176,4 @@ awsctl bastion awsctl rds awsctl eks awsctl ecr +``` From ecaf109da16274172ed1a2490e31daa28a33b178 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Thu, 3 Jul 2025 11:05:38 +0545 Subject: [PATCH 18/51] test: test beta release internal [release] --- .github/workflows/releaser-internal.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index 059b96d..e6fb458 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -46,6 +46,7 @@ jobs: id: new_version run: | CURRENT_TAG=${{ steps.tags.outputs.current_tag }} + echo "Current tag: ${CURRENT_TAG:-}" if [ -z "$CURRENT_TAG" ]; then echo "First release - creating v0.1.0-beta-internal" echo "new_tag=v0.1.0-beta-internal" >> $GITHUB_OUTPUT @@ -56,6 +57,19 @@ jobs: NEW_VERSION=$(semver "$VERSION" -i patch) NEW_TAG="v${NEW_VERSION}-beta-internal" echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + echo "New tag: $NEW_TAG" + + - name: Create release tag + if: steps.new_version.outputs.new_tag + run: | + echo "Creating tag: ${{ steps.new_version.outputs.new_tag }}" + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git tag -a "${{ steps.new_version.outputs.new_tag }}" -m "Release ${{ steps.new_version.outputs.new_tag }}" + git push origin "${{ steps.new_version.outputs.new_tag }}" + echo "Tag pushed successfully" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 From 99578766b8ba3d1eaa1053d6f1998ef9f0627be6 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Thu, 3 Jul 2025 15:03:14 +0545 Subject: [PATCH 19/51] feat: removed prompt to set profile as default on sso init --- internal/sso/profile.go | 90 +++++++++++++++++++++++++++++++++++++++ internal/sso/sso.go | 94 +++-------------------------------------- 2 files changed, 95 insertions(+), 89 deletions(-) diff --git a/internal/sso/profile.go b/internal/sso/profile.go index 8be414f..bbd2be5 100644 --- a/internal/sso/profile.go +++ b/internal/sso/profile.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" ) func (c *RealSSOClient) ConfigureSSOProfile(profile, region, accountID, role, ssoStartUrl, ssoSession string) error { @@ -160,3 +161,92 @@ func (c *RealSSOClient) promptProfileDetails(ssoRegion string) (string, string, } return profileName, region, nil } + +func (c *RealSSOClient) setProfileAsDefault(profile string) error { + sessionName, err := c.ConfigureGet("sso_session", profile) + if err != nil { + return fmt.Errorf("failed to get sso_session: %w", err) + } + + ssoStartURL, err := c.ConfigureGet("sso_start_url", profile) + if err != nil { + return fmt.Errorf("failed to get sso_start_url: %w", err) + } + + ssoRegion, err := c.ConfigureGet("sso_region", profile) + if err != nil { + return fmt.Errorf("failed to get sso_region: %w", err) + } + + accountID, err := c.ConfigureGet("sso_account_id", profile) + if err != nil { + return fmt.Errorf("failed to get account ID: %w", err) + } + + roleName, err := c.ConfigureGet("sso_role_name", profile) + if err != nil { + return fmt.Errorf("failed to get role name: %w", err) + } + + region, err := c.ConfigureGet("region", profile) + if err != nil { + region = ssoRegion + } + + if err := c.configureAWSProfile("default", sessionName, ssoRegion, ssoStartURL, accountID, roleName, region); err != nil { + return fmt.Errorf("failed to configure AWS default profile: %w", err) + } + fmt.Println("Successfully set this profile as default!") + return nil +} + +func (c *RealSSOClient) printProfileSummary(profile string) error { + sessionName, err := c.ConfigureGet("sso_session", profile) + if err != nil { + return fmt.Errorf("failed to get sso_session: %w", err) + } + + ssoStartURL, err := c.ConfigureGet("sso_start_url", profile) + if err != nil { + return fmt.Errorf("failed to get sso_start_url: %w", err) + } + + ssoRegion, err := c.ConfigureGet("sso_region", profile) + if err != nil { + return fmt.Errorf("failed to get sso_region: %w", err) + } + + accountID, err := c.ConfigureGet("sso_account_id", profile) + if err != nil { + return fmt.Errorf("failed to get account ID: %w", err) + } + + roleName, err := c.ConfigureGet("sso_role_name", profile) + if err != nil { + return fmt.Errorf("failed to get role name: %w", err) + } + + roleARN, err := c.AwsSTSGetCallerIdentity(profile) + if err != nil { + return fmt.Errorf("failed to get role ARN: %w", err) + } + + accountName, err := c.GetSSOAccountName(accountID, profile) + if err != nil { + accountName = "Unknown" + fmt.Printf("Warning: Failed to get account name: %v\n", err) + } + + _, expiry, err := c.GetCachedSsoAccessToken(profile) + if err != nil { + return fmt.Errorf("failed to get token expiry: %w", err) + } + + var expiration string + if !expiry.IsZero() { + expiration = expiry.Format(time.RFC3339) + } + + printSummary(profile, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration) + return nil +} diff --git a/internal/sso/sso.go b/internal/sso/sso.go index 2809a66..63c1def 100644 --- a/internal/sso/sso.go +++ b/internal/sso/sso.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "slices" - "time" promptUtils "github.com/BerryBytes/awsctl/utils/prompt" ) @@ -99,48 +98,8 @@ func (c *RealSSOClient) InitSSO(refresh, noBrowser bool) error { } if awsProfile != "default" { - setDefault, err := c.Prompter.PromptYesNo("Set this as the default profile? [Y/n]", true) - if err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return nil - } - return fmt.Errorf("failed to prompt for default profile: %w", err) - } - if setDefault { - sessionName, err := c.ConfigureGet("sso_session", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_session: %w", err) - } - - ssoStartURL, err := c.ConfigureGet("sso_start_url", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_start_url: %w", err) - } - - ssoRegion, err := c.ConfigureGet("sso_region", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_region: %w", err) - } - - accountID, err := c.ConfigureGet("sso_account_id", awsProfile) - if err != nil { - return fmt.Errorf("failed to get account ID: %w", err) - } - - roleName, err := c.ConfigureGet("sso_role_name", awsProfile) - if err != nil { - return fmt.Errorf("failed to get role name: %w", err) - } - - region, err := c.ConfigureGet("region", awsProfile) - if err != nil { - region = ssoRegion - } - - if err := c.configureAWSProfile("default", sessionName, ssoRegion, ssoStartURL, accountID, roleName, region); err != nil { - return fmt.Errorf("failed to configure AWS default profile: %w", err) - } - fmt.Println("Successfully set this profile as default!") + if err := c.setProfileAsDefault(awsProfile); err != nil { + return err } } } @@ -149,59 +108,16 @@ func (c *RealSSOClient) InitSSO(refresh, noBrowser bool) error { return fmt.Errorf("invalid profile: %s", awsProfile) } - var expiration string - _, expiry, err := c.GetCachedSsoAccessToken(awsProfile) - if err != nil { + if _, _, err := c.GetCachedSsoAccessToken(awsProfile); err != nil { fmt.Printf("SSO token expired or missing for profile %s. Logging in...\n", awsProfile) if err := c.SSOLogin(awsProfile, refresh, noBrowser); err != nil { return fmt.Errorf("failed to login: %w", err) } - _, expiry, err = c.GetCachedSsoAccessToken(awsProfile) - if err != nil { + if _, _, err = c.GetCachedSsoAccessToken(awsProfile); err != nil { return fmt.Errorf("failed to get SSO token after login: %w", err) } } - if !expiry.IsZero() { - expiration = expiry.Format(time.RFC3339) - } fmt.Printf("SSO token validated for profile %s\n", awsProfile) - sessionName, err := c.ConfigureGet("sso_session", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_session: %w", err) - } - - ssoStartURL, err := c.ConfigureGet("sso_start_url", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_start_url: %w", err) - } - - ssoRegion, err := c.ConfigureGet("sso_region", awsProfile) - if err != nil { - return fmt.Errorf("failed to get sso_region: %w", err) - } - - accountID, err := c.ConfigureGet("sso_account_id", awsProfile) - if err != nil { - return fmt.Errorf("failed to get account ID: %w", err) - } - - roleName, err := c.ConfigureGet("sso_role_name", awsProfile) - if err != nil { - return fmt.Errorf("failed to get role name: %w", err) - } - - roleARN, err := c.AwsSTSGetCallerIdentity(awsProfile) - if err != nil { - return fmt.Errorf("failed to get role ARN: %w", err) - } - - accountName, err := c.GetSSOAccountName(accountID, awsProfile) - if err != nil { - accountName = "Unknown" - fmt.Printf("Warning: Failed to get account name: %v\n", err) - } - - printSummary(awsProfile, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration) - return nil + return c.printProfileSummary(awsProfile) } From 3464f6b57905afb4d44ef4174fdbe20b1658d136 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 11:34:25 +0545 Subject: [PATCH 20/51] test: beta internal release [release] From 7021baa7bc061979c89d17ec612a58a8a89b3d45 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 12:06:11 +0545 Subject: [PATCH 21/51] test: beta internal release [release] From 812e812f53adf0e8d127f2ce9607bf414b91cf81 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 13:30:14 +0545 Subject: [PATCH 22/51] Trigger workflow [release] From 5f346d4b33b872b175e666985a82cf5ffd57cac4 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 13:38:39 +0545 Subject: [PATCH 23/51] test: testing release [release] --- .github/workflows/releaser-internal.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index e6fb458..b9cd20a 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -8,9 +8,9 @@ on: - "**.md" workflow_dispatch: -concurrency: - group: release-develop - cancel-in-progress: true +# concurrency: +# group: release-develop +# cancel-in-progress: true permissions: contents: write From 5d1c754ba62e8b2162cc5e120fb9a837235e2349 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:18:51 +0545 Subject: [PATCH 24/51] docs: changed the generate changelog script to use flag for generating release notes only --- .github/scripts/generate-changelog.sh | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/scripts/generate-changelog.sh b/.github/scripts/generate-changelog.sh index 5de23fd..47305c7 100755 --- a/.github/scripts/generate-changelog.sh +++ b/.github/scripts/generate-changelog.sh @@ -150,7 +150,16 @@ generate_release_notes() { # Main execution main() { - # Generate changelog + # Handle --notes-only flag + if [[ "$*" == *"--notes-only"* ]]; then + generate_release_notes + if command -v prettier >/dev/null 2>&1; then + prettier --write "$RELEASE_NOTES_FILE" + fi + # echo "Release notes generated at $RELEASE_NOTES_FILE" + return 0 + fi + if [ -f "$CHANGELOG_FILE" ]; then echo "Updating existing changelog..." generate_changelog_content >"$TEMP_FILE" @@ -162,17 +171,12 @@ main() { generate_changelog_content >"$CHANGELOG_FILE" fi - # Generate release notes generate_release_notes - # Format files if Prettier is available if command -v prettier >/dev/null 2>&1; then prettier --write "$CHANGELOG_FILE" "$RELEASE_NOTES_FILE" - else - echo "Note: Prettier is not installed. Skipping formatting." fi - # Update GitHub release if [ -n "${GITHUB_ACTIONS:-}" ] && [ -n "${GITHUB_TOKEN:-}" ]; then if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release edit "$RELEASE_TAG" --notes-file "$RELEASE_NOTES_FILE" @@ -184,4 +188,10 @@ main() { echo "Changelog ($CHANGELOG_FILE) and release notes ($RELEASE_NOTES_FILE) generated successfully" } +# Handle --notes-only flag +if [[ "$*" == *"--notes-only"* ]]; then + main --notes-only + exit 0 +fi + main "$@" From 2a83c89240e8d8d41be5dac3b6496bc124e02293 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:20:00 +0545 Subject: [PATCH 25/51] feat: added test feat to test release note for int release From 23de57eef709b8c47b2e569062db0a475e8f43cd Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:20:12 +0545 Subject: [PATCH 26/51] fix: added test fix to test release note for int release From 4bf1178b240ba0bf6c633423d4202fceb603ea02 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:20:35 +0545 Subject: [PATCH 27/51] docs: added test doc to test release note for int release [release] From 00f46e88f9618efcb48fce8b21aaee1d050db934 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:53:45 +0545 Subject: [PATCH 28/51] test: added release note for beta release --- .github/workflows/releaser-internal.yaml | 30 ++++++++++++++++++- .goreleaser-internal.yaml | 37 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .goreleaser-internal.yaml diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index b9cd20a..e153325 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -35,6 +35,7 @@ jobs: - name: Install dependencies run: | npm install --global semver + sudo apt-get -y install gh - name: Get current tag id: tags @@ -71,11 +72,38 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Generate Release Notes + if: steps.new_version.outputs.new_tag + env: + RELEASE_TAG: ${{ steps.new_version.outputs.new_tag }} + PREVIOUS_TAG: ${{ steps.tags.outputs.current_tag }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Make script executable + chmod +x .github/scripts/generate-changelog.sh + + # Generate notes + .github/scripts/generate-changelog.sh --notes-only + + # Add beta header to existing file (instead of renaming) + echo -e "## 🧪 BETA RELEASE\n*For testing purposes only*\n\n$(cat RELEASE_NOTES.md)" > RELEASE_NOTES.md + + # Store for GoReleaser + echo "NOTES=$(cat RELEASE_NOTES.md)" >> $GITHUB_ENV + + # Keep your prettier formatting + if command -v prettier >/dev/null 2>&1; then + prettier --write RELEASE_NOTES.md + # Update the env variable if formatting changed it + echo "NOTES=$(cat RELEASE_NOTES.md)" >> $GITHUB_ENV + fi + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: v2.9.0 - args: release --clean + args: release --clean -f ./.goreleaser-internal.yaml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NOTES: ${{ env.NOTES }} diff --git a/.goreleaser-internal.yaml b/.goreleaser-internal.yaml new file mode 100644 index 0000000..68e6257 --- /dev/null +++ b/.goreleaser-internal.yaml @@ -0,0 +1,37 @@ +version: 2 +project_name: awsctl + +before: + hooks: + - go mod tidy + - go mod verify + +builds: + - env: + - CGO_ENABLED=0 + binary: awsctl + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X main.Version={{.Version}}-beta-internal + +release: + github: + owner: berrybytes + name: awsctl + prerelease: true # Marks as pre-release in GitHub + name_template: "{{.Version}}-beta-internal" + notes: | + {{ .Env.NOTES }} + extra_files: + - glob: ./LICENSE + - glob: ./README.md + footer: >- + --- + BETA RELEASE - FOR TESTING ONLY + Released by [GoReleaser](https://github.com/goreleaser/goreleaser). From f51763580eabfc798dd1b18918a2f9f1f41575d2 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:54:24 +0545 Subject: [PATCH 29/51] feat: added test feat to test release note for int release From bf9c2d5db7a0ec1ce2b0693c3a46dba508f749e5 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:54:33 +0545 Subject: [PATCH 30/51] fix: added test fix to test release note for int release From 42e460793d74accfab056f063ef4204265a94db6 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 15:54:36 +0545 Subject: [PATCH 31/51] docs: added test doc to test release note for int release [release] From d6994e3bf680a819f10a8fc5a853a83c9d19914c Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:06:55 +0545 Subject: [PATCH 32/51] test: changed genrate release note script [release] --- .github/workflows/releaser-internal.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index e153325..37b03d3 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -88,14 +88,22 @@ jobs: # Add beta header to existing file (instead of renaming) echo -e "## 🧪 BETA RELEASE\n*For testing purposes only*\n\n$(cat RELEASE_NOTES.md)" > RELEASE_NOTES.md - # Store for GoReleaser - echo "NOTES=$(cat RELEASE_NOTES.md)" >> $GITHUB_ENV + # Store for GoReleaser (multi-line safe) + { + echo "NOTES<> $GITHUB_ENV # Keep your prettier formatting if command -v prettier >/dev/null 2>&1; then prettier --write RELEASE_NOTES.md # Update the env variable if formatting changed it - echo "NOTES=$(cat RELEASE_NOTES.md)" >> $GITHUB_ENV + { + echo "NOTES<> $GITHUB_ENV fi - name: Run GoReleaser From d8644715d0b50c77c721deb8b631848626f484fd Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:13:15 +0545 Subject: [PATCH 33/51] test: test beta release with release notes [release] --- .github/workflows/releaser-internal.yaml | 2 +- .goreleaser-internal.yaml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index 37b03d3..a1707c0 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -111,7 +111,7 @@ jobs: with: distribution: goreleaser version: v2.9.0 - args: release --clean -f ./.goreleaser-internal.yaml + args: release --clean -f ./.goreleaser-internal.yaml --release-notes RELEASE_NOTES.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NOTES: ${{ env.NOTES }} diff --git a/.goreleaser-internal.yaml b/.goreleaser-internal.yaml index 68e6257..eacad76 100644 --- a/.goreleaser-internal.yaml +++ b/.goreleaser-internal.yaml @@ -26,8 +26,6 @@ release: name: awsctl prerelease: true # Marks as pre-release in GitHub name_template: "{{.Version}}-beta-internal" - notes: | - {{ .Env.NOTES }} extra_files: - glob: ./LICENSE - glob: ./README.md From 19adbb6e1be2a25f2bd4d05e9091ceb037d7d702 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:22:43 +0545 Subject: [PATCH 34/51] docs: added test doc to test release note for int release [release] From d8a07f575d99246207d3bc82a9c93f063d5f8b97 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:28:06 +0545 Subject: [PATCH 35/51] docs: added test doc to test release note for int release [release] From b1782605de0736e30adfd2783da0e49c482179c6 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:30:52 +0545 Subject: [PATCH 36/51] chore: test release notes [release] --- .github/workflows/releaser-internal.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index a1707c0..ed5861d 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -114,4 +114,3 @@ jobs: args: release --clean -f ./.goreleaser-internal.yaml --release-notes RELEASE_NOTES.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NOTES: ${{ env.NOTES }} From 1a9c8ede04d9f29a4e1d28324f261dd4cad051f0 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 16:42:25 +0545 Subject: [PATCH 37/51] test: test beta release with release notes and removed pre-release [release] --- .goreleaser-internal.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.goreleaser-internal.yaml b/.goreleaser-internal.yaml index eacad76..950ea51 100644 --- a/.goreleaser-internal.yaml +++ b/.goreleaser-internal.yaml @@ -24,7 +24,6 @@ release: github: owner: berrybytes name: awsctl - prerelease: true # Marks as pre-release in GitHub name_template: "{{.Version}}-beta-internal" extra_files: - glob: ./LICENSE From 9a29a53d3e1b557754854f7576464d5a0519df4d Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 4 Jul 2025 17:00:32 +0545 Subject: [PATCH 38/51] test: fixed double suffix in beta release [release] --- .github/workflows/releaser-internal.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index ed5861d..75e3ebe 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -56,7 +56,7 @@ jobs: VERSION=$(echo "$CURRENT_TAG" | sed 's/^v//' | sed 's/-beta-internal//') NEW_VERSION=$(semver "$VERSION" -i patch) - NEW_TAG="v${NEW_VERSION}-beta-internal" + NEW_TAG="v${NEW_VERSION}" echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT echo "New tag: $NEW_TAG" From adff76ec748eca9f891aa496cfb101707a891864 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 14:54:16 +0545 Subject: [PATCH 39/51] feat: added name, region, start url on sso setup command --- cmd/sso/setup.go | 36 +++++++- cmd/sso/setup_test.go | 160 +++++++++++++++++++++++++++++++--- internal/sso/client.go | 11 ++- internal/sso/client_test.go | 61 ++++++------- internal/sso/interface.go | 2 +- internal/sso/profile.go | 12 +-- internal/sso/profile_test.go | 15 ++-- internal/sso/prompter.go | 16 ++-- internal/sso/prompter_test.go | 27 +++--- internal/sso/session.go | 104 +++++++++++----------- internal/sso/session_test.go | 47 ++++++---- internal/sso/sso.go | 34 ++++---- internal/sso/utils.go | 12 +-- internal/sso/utils_test.go | 11 +-- tests/mock/sso/sso.go | 9 +- utils/general/general.go | 6 ++ 16 files changed, 382 insertions(+), 181 deletions(-) diff --git a/cmd/sso/setup.go b/cmd/sso/setup.go index eb942c2..21554f3 100644 --- a/cmd/sso/setup.go +++ b/cmd/sso/setup.go @@ -3,19 +3,45 @@ package sso import ( "errors" "fmt" + "strings" "github.com/BerryBytes/awsctl/internal/sso" + generalutils "github.com/BerryBytes/awsctl/utils/general" promptUtils "github.com/BerryBytes/awsctl/utils/prompt" "github.com/spf13/cobra" ) func SetupCmd(ssoClient sso.SSOClient) *cobra.Command { - return &cobra.Command{ + var startURL string + var region string + var name string + + cmd := &cobra.Command{ Use: "setup", Short: "Setup AWS SSO configuration", RunE: func(cmd *cobra.Command, args []string) error { - err := ssoClient.SetupSSO() + if startURL != "" && !strings.HasPrefix(startURL, "https://") { + return fmt.Errorf("invalid start URL: must begin with https://") + } + + if region != "" { + if !generalutils.IsRegionValid(region) { + return fmt.Errorf("invalid AWS region: %s", region) + } + } + + if name != "" && !generalutils.IsValidSessionName(name) { + return fmt.Errorf("invalid session name: must only contain letters, numbers, dashes, or underscores, and cannot start or end with a dash/underscore") + } + + opts := sso.SSOFlagOptions{ + StartURL: startURL, + Region: region, + Name: name, + } + + err := ssoClient.SetupSSO(opts) if err != nil { if errors.Is(err, promptUtils.ErrInterrupted) { return nil @@ -25,4 +51,10 @@ func SetupCmd(ssoClient sso.SSOClient) *cobra.Command { return nil }, } + + cmd.Flags().StringVar(&name, "name", "", "SSO session name") + cmd.Flags().StringVar(&startURL, "start-url", "", "AWS SSO Start URL") + cmd.Flags().StringVar(®ion, "region", "", "AWS SSO Region") + + return cmd } diff --git a/cmd/sso/setup_test.go b/cmd/sso/setup_test.go index 8bc39b3..203acb1 100644 --- a/cmd/sso/setup_test.go +++ b/cmd/sso/setup_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" + "github.com/BerryBytes/awsctl/internal/sso" mock_sso "github.com/BerryBytes/awsctl/tests/mock/sso" promptUtils "github.com/BerryBytes/awsctl/utils/prompt" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" ) @@ -18,30 +18,86 @@ func TestSetupCmd(t *testing.T) { mockSSOClient := mock_sso.NewMockSSOClient(ctrl) tests := []struct { - name string - mockSetup func() - expectedError string + name string + args []string + mockSetup func() + expectedError string + expectedOutput string }{ { - name: "successful setup", + name: "successful setup with no flags", + args: []string{}, + mockSetup: func() { + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{}).Return(nil) + }, + }, + { + name: "successful setup with all flags", + args: []string{"--name=test-session", "--start-url=https://test.awsapps.com/start", "--region=us-east-1"}, mockSetup: func() { - mockSSOClient.EXPECT().SetupSSO().Return(nil) + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{ + Name: "test-session", + StartURL: "https://test.awsapps.com/start", + Region: "us-east-1", + }).Return(nil) }, - expectedError: "", }, { name: "error during setup", + args: []string{}, mockSetup: func() { - mockSSOClient.EXPECT().SetupSSO().Return(errors.New("setup error")) + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{}).Return(errors.New("setup error")) }, - expectedError: "setup error", + expectedError: "SSO initialization failed: setup error", }, { name: "interrupted by user", + args: []string{}, mockSetup: func() { - mockSSOClient.EXPECT().SetupSSO().Return(promptUtils.ErrInterrupted) + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{}).Return(promptUtils.ErrInterrupted) + }, + }, + { + name: "invalid start URL format", + args: []string{"--start-url=invalid-url"}, + mockSetup: func() { + + }, + expectedError: "invalid start URL: must begin with https://", + }, + { + name: "invalid region", + args: []string{"--region=invalid-region"}, + mockSetup: func() { + + }, + expectedError: "invalid AWS region: invalid-region", + }, + { + name: "invalid session name", + args: []string{"--name=invalid-name-"}, + mockSetup: func() { + + }, + expectedError: "invalid session name: must only contain letters, numbers, dashes, or underscores, and cannot start or end with a dash/underscore", + }, + { + name: "partial flags - only name provided", + args: []string{"--name=valid-name"}, + mockSetup: func() { + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{ + Name: "valid-name", + }).Return(nil) + }, + }, + { + name: "partial flags - only region provided", + args: []string{"--region=us-west-2"}, + mockSetup: func() { + mockSSOClient.EXPECT().SetupSSO(sso.SSOFlagOptions{ + Region: "us-west-2", + }).Return(nil) }, - expectedError: "", }, } @@ -50,15 +106,91 @@ func TestSetupCmd(t *testing.T) { tt.mockSetup() cmd := SetupCmd(mockSSOClient) - cmd.SetArgs([]string{}) + cmd.SetArgs(tt.args) err := cmd.Execute() - if tt.expectedError == "" { - assert.NoError(t, err) + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) } else { + assert.NoError(t, err) + } + }) + } +} + +func TestSetupCmd_FlagValidation(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSSOClient := mock_sso.NewMockSSOClient(ctrl) + + tests := []struct { + name string + args []string + expectCall bool + expectedError string + }{ + { + name: "valid start URL", + args: []string{"--start-url=https://valid.awsapps.com/start"}, + expectCall: true, + }, + { + name: "invalid start URL missing https", + args: []string{"--start-url=http://invalid.awsapps.com/start"}, + expectCall: false, + expectedError: "invalid start URL: must begin with https://", + }, + { + name: "invalid start URL format", + args: []string{"--start-url=invalid-format"}, + expectCall: false, + expectedError: "invalid start URL: must begin with https://", + }, + { + name: "valid region", + args: []string{"--region=eu-west-1"}, + expectCall: true, + }, + { + name: "invalid region", + args: []string{"--region=invalid-region"}, + expectCall: false, + expectedError: "invalid AWS region: invalid-region", + }, + { + name: "valid session name", + args: []string{"--name=valid_name-123"}, + expectCall: true, + }, + { + name: "invalid session name starts with dash", + args: []string{"--name=-invalid"}, + expectCall: false, + expectedError: "invalid session name: must only contain letters, numbers, dashes, or underscores, and cannot start or end with a dash/underscore", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.expectCall { + mockSSOClient.EXPECT().SetupSSO(gomock.Any()).Return(nil) + } + + cmd := SetupCmd(mockSSOClient) + cmd.SetArgs(tt.args) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + err := cmd.Execute() + + if tt.expectedError != "" { assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) } }) } diff --git a/internal/sso/client.go b/internal/sso/client.go index a4fa885..c80984a 100644 --- a/internal/sso/client.go +++ b/internal/sso/client.go @@ -23,6 +23,11 @@ type RealSSOClient struct { Prompter Prompter Executor common.CommandExecutor } +type SSOFlagOptions struct { + StartURL string + Region string + Name string +} func NewSSOClient(prompter Prompter, executor common.CommandExecutor) (SSOClient, error) { if prompter == nil { @@ -42,7 +47,7 @@ func NewSSOClient(prompter Prompter, executor common.CommandExecutor) (SSOClient }, nil } -func (c *RealSSOClient) getSsoAccessTokenFromCache(profile string) (*models.SSOCache, time.Time, error) { +func (c *RealSSOClient) GetSsoAccessTokenFromCache(profile string) (*models.SSOCache, time.Time, error) { startURL, err := c.ConfigureGet("sso_start_url", profile) if err != nil { return nil, time.Time{}, fmt.Errorf("failed to get sso_start_url for profile %s: %v", profile, err) @@ -112,7 +117,7 @@ func (c *RealSSOClient) getSsoAccessTokenFromCache(profile string) (*models.SSOC if err != nil { return nil, time.Time{}, fmt.Errorf("SSO login failed: %v", err) } - selectedCache, expiryTime, err = c.getSsoAccessTokenFromCache(profile) + selectedCache, expiryTime, err = c.GetSsoAccessTokenFromCache(profile) if err != nil { return nil, time.Time{}, fmt.Errorf("failed to get token after re-login: %v", err) } @@ -130,7 +135,7 @@ func (c *RealSSOClient) GetCachedSsoAccessToken(profile string) (string, time.Ti return c.TokenCache.AccessToken, c.TokenCache.Expiry, nil } - cachedSSO, expiry, err := c.getSsoAccessTokenFromCache(profile) + cachedSSO, expiry, err := c.GetSsoAccessTokenFromCache(profile) if err != nil { return "", time.Time{}, err } diff --git a/internal/sso/client_test.go b/internal/sso/client_test.go index f600e63..eb7cbfc 100644 --- a/internal/sso/client_test.go +++ b/internal/sso/client_test.go @@ -1,4 +1,4 @@ -package sso +package sso_test import ( "context" @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/BerryBytes/awsctl/internal/sso" "github.com/BerryBytes/awsctl/models" mock_awsctl "github.com/BerryBytes/awsctl/tests/mock" mock_sso "github.com/BerryBytes/awsctl/tests/mock/sso" @@ -27,13 +28,13 @@ func TestNewSSOClient(t *testing.T) { mockPrompter := mock_sso.NewMockPrompter(ctrl) mockExecutor := mock_awsctl.NewMockCommandExecutor(ctrl) - client, err := NewSSOClient(mockPrompter, mockExecutor) + client, err := sso.NewSSOClient(mockPrompter, mockExecutor) assert.NoError(t, err) assert.NotNil(t, client) }) t.Run("nil prompter returns error", func(t *testing.T) { - client, err := NewSSOClient(nil, nil) + client, err := sso.NewSSOClient(nil, nil) assert.Error(t, err) assert.Nil(t, client) assert.Equal(t, "prompter cannot be nil", err.Error()) @@ -46,10 +47,10 @@ func TestNewSSOClient_NilExecutor(t *testing.T) { mockPrompter := mock_sso.NewMockPrompter(ctrl) - client, err := NewSSOClient(mockPrompter, nil) + client, err := sso.NewSSOClient(mockPrompter, nil) assert.NoError(t, err) assert.NotNil(t, client) - assert.IsType(t, &common.RealCommandExecutor{}, client.(*RealSSOClient).Executor) + assert.IsType(t, &common.RealCommandExecutor{}, client.(*sso.RealSSOClient).Executor) }) } @@ -61,7 +62,7 @@ func TestGetCachedSsoAccessToken(t *testing.T) { mockExecutor := mock_awsctl.NewMockCommandExecutor(ctrl) t.Run("returns cached token if valid", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, TokenCache: models.TokenCache{ @@ -96,7 +97,7 @@ func TestGetCachedSsoAccessToken(t *testing.T) { mockExecutor.EXPECT().RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "test-profile"). Return([]byte("https://example.awsapps.com/start"), nil) - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, TokenCache: models.TokenCache{ @@ -127,7 +128,7 @@ func TestAwsSTSGetCallerIdentity(t *testing.T) { mockExecutor := mock_awsctl.NewMockCommandExecutor(ctrl) t.Run("successful identity retrieval", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -147,7 +148,7 @@ func TestAwsSTSGetCallerIdentity(t *testing.T) { }) t.Run("command failure", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -218,7 +219,7 @@ func TestSSOLogin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -242,7 +243,7 @@ func TestGetSSOAccountName(t *testing.T) { tests := []struct { name string - setup func(client *RealSSOClient) + setup func(client *sso.RealSSOClient) accountID string profile string expectedName string @@ -251,7 +252,7 @@ func TestGetSSOAccountName(t *testing.T) { }{ { name: "successful account name retrieval with cache hit", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { client.TokenCache.AccessToken = "test-token" client.TokenCache.Expiry = time.Now().Add(1 * time.Hour) @@ -272,7 +273,7 @@ func TestGetSSOAccountName(t *testing.T) { }, { name: "successful account name retrieval with cache miss", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { tempDir := t.TempDir() cacheDir := filepath.Join(tempDir, ".aws", "sso", "cache") require.NoError(t, os.MkdirAll(cacheDir, 0755)) @@ -315,7 +316,7 @@ func TestGetSSOAccountName(t *testing.T) { }, { name: "account not found", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { tempDir := t.TempDir() cacheDir := filepath.Join(tempDir, ".aws", "sso", "cache") require.NoError(t, os.MkdirAll(cacheDir, 0755)) @@ -358,7 +359,7 @@ func TestGetSSOAccountName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Executor: mockExecutor, Prompter: mockPrompter, } @@ -387,7 +388,7 @@ func TestGetRoleCredentials(t *testing.T) { mockExecutor := mock_awsctl.NewMockCommandExecutor(ctrl) t.Run("successful credentials retrieval", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -423,7 +424,7 @@ func TestGetRoleCredentials(t *testing.T) { }) t.Run("command failure", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -440,7 +441,7 @@ func TestGetRoleCredentials(t *testing.T) { }) t.Run("invalid JSON response", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -469,7 +470,7 @@ func TestGetCachedSsoAccessToken_ErrorCases(t *testing.T) { RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "bad-profile"). Return(nil, errors.New("config error")) - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -505,7 +506,7 @@ func TestGetCachedSsoAccessToken_ErrorCases(t *testing.T) { _ = os.Setenv("HOME", oldHome) }) - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -524,7 +525,7 @@ func TestGetSSOAccountName_ErrorCases(t *testing.T) { mockExecutor := mock_awsctl.NewMockCommandExecutor(ctrl) t.Run("error getting access token", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -539,7 +540,7 @@ func TestGetSSOAccountName_ErrorCases(t *testing.T) { }) t.Run("error listing accounts", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, TokenCache: models.TokenCache{ @@ -558,7 +559,7 @@ func TestGetSSOAccountName_ErrorCases(t *testing.T) { }) t.Run("invalid accounts JSON", func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, TokenCache: models.TokenCache{ @@ -586,14 +587,14 @@ func TestGetSsoAccessTokenFromCache_ErrorCases(t *testing.T) { tests := []struct { name string - setup func(*RealSSOClient) + setup func(*sso.RealSSOClient) profile string expectedError string expectError bool }{ { name: "error getting start URL", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { mockExecutor.EXPECT(). RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "bad-profile"). Return(nil, errors.New("config error")) @@ -604,7 +605,7 @@ func TestGetSsoAccessTokenFromCache_ErrorCases(t *testing.T) { }, { name: "error getting home directory", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { mockExecutor.EXPECT(). RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "test-profile"). Return([]byte("https://example.com"), nil) @@ -623,7 +624,7 @@ func TestGetSsoAccessTokenFromCache_ErrorCases(t *testing.T) { }, { name: "cache directory not exists", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { mockExecutor.EXPECT(). RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "test-profile"). Return([]byte("https://example.com"), nil) @@ -643,7 +644,7 @@ func TestGetSsoAccessTokenFromCache_ErrorCases(t *testing.T) { }, { name: "no matching cache file", - setup: func(client *RealSSOClient) { + setup: func(client *sso.RealSSOClient) { mockExecutor.EXPECT(). RunCommand("aws", "configure", "get", "sso_start_url", "--profile", "test-profile"). Return([]byte("https://example.com"), nil) @@ -677,13 +678,13 @@ func TestGetSsoAccessTokenFromCache_ErrorCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } tt.setup(client) - cache, _, err := client.getSsoAccessTokenFromCache(tt.profile) + cache, _, err := client.GetSsoAccessTokenFromCache(tt.profile) if tt.expectError { require.Error(t, err) diff --git a/internal/sso/interface.go b/internal/sso/interface.go index 8e031de..2684a72 100644 --- a/internal/sso/interface.go +++ b/internal/sso/interface.go @@ -7,7 +7,7 @@ import ( ) type SSOClient interface { - SetupSSO() error + SetupSSO(opts SSOFlagOptions) error InitSSO(refresh, noBrowser bool) error ConfigureSet(key, value, profile string) error ConfigureGet(key, profile string) (string, error) diff --git a/internal/sso/profile.go b/internal/sso/profile.go index bbd2be5..2e763f9 100644 --- a/internal/sso/profile.go +++ b/internal/sso/profile.go @@ -28,12 +28,12 @@ func (c *RealSSOClient) ConfigureSSOProfile(profile, region, accountID, role, ss return nil } -func (c *RealSSOClient) configureAWSProfile(profileName, sessionName, ssoRegion, ssoStartURL, accountID, roleName, region string) error { +func (c *RealSSOClient) ConfigureAWSProfile(profileName, sessionName, ssoRegion, ssoStartURL, accountID, roleName, region string) error { ssoStartURL = strings.TrimSuffix(ssoStartURL, "#") - if err := validateStartURL(ssoStartURL); err != nil { + if err := ValidateStartURL(ssoStartURL); err != nil { return fmt.Errorf("invalid start URL: %w", err) } - if err := validateAccountID(accountID); err != nil { + if err := ValidateAccountID(accountID); err != nil { return fmt.Errorf("invalid account ID: %w", err) } @@ -150,7 +150,7 @@ func (c *RealSSOClient) configureAWSProfile(profileName, sessionName, ssoRegion, return nil } -func (c *RealSSOClient) promptProfileDetails(ssoRegion string) (string, string, error) { +func (c *RealSSOClient) PromptProfileDetails(ssoRegion string) (string, string, error) { profileName, err := c.Prompter.PromptWithDefault("Enter profile name to configure", "sso-profile") if err != nil { return "", "", fmt.Errorf("failed to prompt for profile name: %w", err) @@ -193,7 +193,7 @@ func (c *RealSSOClient) setProfileAsDefault(profile string) error { region = ssoRegion } - if err := c.configureAWSProfile("default", sessionName, ssoRegion, ssoStartURL, accountID, roleName, region); err != nil { + if err := c.ConfigureAWSProfile("default", sessionName, ssoRegion, ssoStartURL, accountID, roleName, region); err != nil { return fmt.Errorf("failed to configure AWS default profile: %w", err) } fmt.Println("Successfully set this profile as default!") @@ -247,6 +247,6 @@ func (c *RealSSOClient) printProfileSummary(profile string) error { expiration = expiry.Format(time.RFC3339) } - printSummary(profile, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration) + PrintSummary(profile, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration) return nil } diff --git a/internal/sso/profile_test.go b/internal/sso/profile_test.go index cdd8fd4..c528a39 100644 --- a/internal/sso/profile_test.go +++ b/internal/sso/profile_test.go @@ -1,4 +1,4 @@ -package sso +package sso_test import ( "errors" @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/BerryBytes/awsctl/internal/sso" mock_awsctl "github.com/BerryBytes/awsctl/tests/mock" mock_sso "github.com/BerryBytes/awsctl/tests/mock/sso" "github.com/golang/mock/gomock" @@ -29,7 +30,7 @@ func TestConfigureSSOProfile(t *testing.T) { mockExecutor.EXPECT().RunCommand("aws", "configure", "set", "region", "us-west-2", "--profile", "test-profile").Return(nil, nil) mockExecutor.EXPECT().RunCommand("aws", "configure", "set", "output", "json", "--profile", "test-profile").Return(nil, nil) - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -45,7 +46,7 @@ func TestConfigureSSOProfile(t *testing.T) { mockExecutor.EXPECT().RunCommand(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } @@ -180,12 +181,12 @@ func TestConfigureAWSProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Executor: mockExecutor, } - err := client.configureAWSProfile(tt.profileName, tt.sessionName, tt.ssoRegion, tt.ssoStartURL, tt.accountID, tt.roleName, tt.region) + err := client.ConfigureAWSProfile(tt.profileName, tt.sessionName, tt.ssoRegion, tt.ssoStartURL, tt.accountID, tt.roleName, tt.region) if tt.expectError { require.Error(t, err) @@ -257,11 +258,11 @@ func TestPromptProfileDetails(t *testing.T) { Return(tt.mockResponses[2], tt.mockResponses[3]) } - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, } - name, region, err := client.promptProfileDetails(tt.ssoRegion) + name, region, err := client.PromptProfileDetails(tt.ssoRegion) if tt.expectError { assert.Error(t, err) diff --git a/internal/sso/prompter.go b/internal/sso/prompter.go index a8c9e58..3eb5ec7 100644 --- a/internal/sso/prompter.go +++ b/internal/sso/prompter.go @@ -30,7 +30,7 @@ func (r *RealPromptRunner) RunSelect(label string, items []string) (string, erro return result, err } -var validateStartURLFunc = func(input string) error { +var ValidateStartURLFunc = func(input string) error { if !strings.HasPrefix(input, "http://") && !strings.HasPrefix(input, "https://") { return fmt.Errorf("invalid URL format") } @@ -39,7 +39,7 @@ var validateStartURLFunc = func(input string) error { type PromptUI struct { Prompt promptUtils.Prompter - runner PromptRunner + Runner PromptRunner } func handlePromptError(err error) error { @@ -60,7 +60,7 @@ func (p *PromptUI) PromptWithDefault(label, defaultValue string) (string, error) } return nil } - result, err := p.runner.RunPrompt(label, defaultValue, validate) + result, err := p.Runner.RunPrompt(label, defaultValue, validate) err = handlePromptError(err) if err != nil { return "", err @@ -89,9 +89,9 @@ func (p *PromptUI) PromptRequired(label string) (string, error) { if strings.TrimSpace(input) == "" { return fmt.Errorf("input is required") } - return validateStartURLFunc(input) + return ValidateStartURLFunc(input) } - result, err := p.runner.RunPrompt(label, "", validate) + result, err := p.Runner.RunPrompt(label, "", validate) err = handlePromptError(err) if err != nil { return "", err @@ -100,7 +100,7 @@ func (p *PromptUI) PromptRequired(label string) (string, error) { } func (p *PromptUI) SelectFromList(label string, items []string) (string, error) { - result, err := p.runner.RunSelect(label, items) + result, err := p.Runner.RunSelect(label, items) err = handlePromptError(err) if err != nil { return "", err @@ -120,7 +120,7 @@ func (p *PromptUI) PromptYesNo(label string, defaultValue bool) (bool, error) { } return nil } - result, err := p.runner.RunPrompt(label, defaultStr, validate) + result, err := p.Runner.RunPrompt(label, defaultStr, validate) err = handlePromptError(err) if err != nil { return false, err @@ -133,5 +133,5 @@ func (p *PromptUI) PromptYesNo(label string, defaultValue bool) (bool, error) { } func NewPrompter() Prompter { - return &PromptUI{runner: &RealPromptRunner{}, Prompt: promptUtils.NewPrompt()} + return &PromptUI{Runner: &RealPromptRunner{}, Prompt: promptUtils.NewPrompt()} } diff --git a/internal/sso/prompter_test.go b/internal/sso/prompter_test.go index 12ea4df..5ccb549 100644 --- a/internal/sso/prompter_test.go +++ b/internal/sso/prompter_test.go @@ -1,4 +1,4 @@ -package sso +package sso_test import ( "errors" @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/BerryBytes/awsctl/internal/sso" mock_awsctl "github.com/BerryBytes/awsctl/tests/mock" mock_sso "github.com/BerryBytes/awsctl/tests/mock/sso" promptUtils "github.com/BerryBytes/awsctl/utils/prompt" @@ -101,7 +102,7 @@ func TestPromptUI_PromptWithDefault(t *testing.T) { RunPrompt(tt.label, tt.defaultValue, gomock.Any()). Return(tt.input, tt.inputErr) - p := &PromptUI{runner: mockRunner} + p := &sso.PromptUI{Runner: mockRunner} result, err := p.PromptWithDefault(tt.label, tt.defaultValue) if tt.wantErr { @@ -173,7 +174,7 @@ func TestPromptUI_SelectFromList(t *testing.T) { RunSelect(tt.label, tt.items). Return(tt.input, tt.inputErr) - p := &PromptUI{runner: mockRunner} + p := &sso.PromptUI{Runner: mockRunner} result, err := p.SelectFromList(tt.label, tt.items) if tt.wantErr { @@ -282,7 +283,7 @@ func TestPromptUI_PromptYesNo(t *testing.T) { RunPrompt(tt.label, defaultStr, gomock.Any()). Return(tt.input, tt.inputErr) - p := &PromptUI{runner: mockRunner} + p := &sso.PromptUI{Runner: mockRunner} result, err := p.PromptYesNo(tt.label, tt.defaultValue) if tt.wantErr { @@ -300,9 +301,9 @@ func TestPromptUI_PromptYesNo(t *testing.T) { } func TestPromptUI_PromptRequired(t *testing.T) { - originalValidateStartURL := validateStartURLFunc - validateStartURLFunc = mockValidateStartURL - defer func() { validateStartURLFunc = originalValidateStartURL }() + originalValidateStartURL := sso.ValidateStartURLFunc + sso.ValidateStartURLFunc = mockValidateStartURL + defer func() { sso.ValidateStartURLFunc = originalValidateStartURL }() tests := []struct { name string @@ -369,7 +370,7 @@ func TestPromptUI_PromptRequired(t *testing.T) { RunPrompt(tt.label, "", gomock.Any()). Return(tt.input, tt.inputErr) - p := &PromptUI{runner: mockRunner} + p := &sso.PromptUI{Runner: mockRunner} result, err := p.PromptRequired(tt.label) if tt.wantErr { @@ -466,7 +467,7 @@ func TestPromptUI_PromptForRegion(t *testing.T) { Return(tt.input, tt.inputErr) } - p := &PromptUI{Prompt: mockPrompter} + p := &sso.PromptUI{Prompt: mockPrompter} result, err := p.PromptForRegion(tt.defaultRegion) if tt.wantErr { @@ -522,7 +523,7 @@ func TestValidateStartURLFunc(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateStartURLFunc(tt.input) + err := sso.ValidateStartURLFunc(tt.input) if tt.wantErr { assert.Error(t, err) if tt.errContains != "" { @@ -536,8 +537,8 @@ func TestValidateStartURLFunc(t *testing.T) { } func TestNewPrompter(t *testing.T) { - prompter := NewPrompter() + prompter := sso.NewPrompter() assert.NotNil(t, prompter) - _, ok := prompter.(*PromptUI) - assert.True(t, ok, "NewPrompter should return a *PromptUI") + _, ok := prompter.(*sso.PromptUI) + assert.True(t, ok, "NewPrompter should return a *sso.PromptUI") } diff --git a/internal/sso/session.go b/internal/sso/session.go index 4c472b8..b309e3a 100644 --- a/internal/sso/session.go +++ b/internal/sso/session.go @@ -15,73 +15,79 @@ import ( promptUtils "github.com/BerryBytes/awsctl/utils/prompt" ) -func (c *RealSSOClient) loadOrCreateSession() (string, *models.SSOSession, error) { +func (c *RealSSOClient) LoadOrCreateSession(name, startURL, region string) (string, *models.SSOSession, error) { configPath, err := config.FindConfigFile(&c.Config) if err != nil && !errors.Is(err, config.ErrNoConfigFile) { return "", nil, fmt.Errorf("failed to check config file: %w", err) } - var ssoSession *models.SSOSession - if configPath != "" { - fileInfo, err := os.Stat(configPath) - if err != nil { - return "", nil, fmt.Errorf("failed to stat config file: %w", err) + if name != "" && startURL != "" && region != "" { + ssoSession := &models.SSOSession{ + Name: name, + StartURL: strings.TrimSuffix(startURL, "#"), + Region: region, + Scopes: "sso:account:access", } - if fileInfo.Size() > 0 && len(c.Config.RawCustomConfig.SSOSessions) > 0 { - fmt.Printf("Loaded existing configuration from '%s'\n", configPath) - if len(c.Config.RawCustomConfig.SSOSessions) == 1 { - ssoSession = &c.Config.RawCustomConfig.SSOSessions[0] - ssoSession.StartURL = strings.TrimSuffix(ssoSession.StartURL, "#") - if ssoSession.Scopes == "" { - ssoSession.Scopes = "sso:account:access" - } - fmt.Printf("Using SSO session: %s (Start URL: %s, Region: %s)\n", - ssoSession.Name, ssoSession.StartURL, ssoSession.Region) - } else { - ssoSession, err = c.selectSSOSession() - - if err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return "", nil, promptUtils.ErrInterrupted - } - return "", nil, fmt.Errorf("failed to select SSO session: %w", err) - } + + c.Config.RawCustomConfig.SSOSessions = append(c.Config.RawCustomConfig.SSOSessions, *ssoSession) + return configPath, ssoSession, nil + } + + if configPath != "" && len(c.Config.RawCustomConfig.SSOSessions) > 0 { + fmt.Printf("Loaded existing configuration from '%s'\n", configPath) + + if len(c.Config.RawCustomConfig.SSOSessions) == 1 && name == "" && startURL == "" && region == "" { + ssoSession := &c.Config.RawCustomConfig.SSOSessions[0] + ssoSession.StartURL = strings.TrimSuffix(ssoSession.StartURL, "#") + if ssoSession.Scopes == "" { + ssoSession.Scopes = "sso:account:access" } + fmt.Printf("Using SSO session: %s (Start URL: %s, Region: %s)\n", + ssoSession.Name, ssoSession.StartURL, ssoSession.Region) + return configPath, ssoSession, nil } - } - if ssoSession == nil || ssoSession.Name == "" || ssoSession.StartURL == "" || ssoSession.Region == "" { - fmt.Println("Setting up a new AWS SSO configuration...") - name, err := c.Prompter.PromptWithDefault("SSO session name", "default-sso") + ssoSession, err := c.SelectSSOSession() if err != nil { - return "", nil, fmt.Errorf("failed to prompt for SSO session name: %w", err) + if errors.Is(err, promptUtils.ErrInterrupted) { + return "", nil, promptUtils.ErrInterrupted + } + return "", nil, fmt.Errorf("failed to select SSO session: %w", err) } - startURL, err := c.Prompter.PromptRequired("SSO start URL (e.g., https://my-sso-portal.awsapps.com/start)") - if err != nil { - return "", nil, fmt.Errorf("failed to prompt for SSO start URL: %w", err) + if ssoSession != nil { + return configPath, ssoSession, nil } + } - region, err := c.Prompter.PromptForRegion("us-east-1") - if err != nil { - return "", nil, fmt.Errorf("failed to prompt for SSO region: %w", err) - } + fmt.Println("Setting up a new AWS SSO configuration...") - scopes := "sso:account:access" + name, err = c.Prompter.PromptWithDefault("SSO session name", "default-sso") + if err != nil { + return "", nil, fmt.Errorf("failed to prompt for SSO session name: %w", err) + } - ssoSession = &models.SSOSession{ - Name: name, - StartURL: strings.TrimSuffix(startURL, "#"), - Region: region, - Scopes: scopes, - } + startURL, err = c.Prompter.PromptRequired("SSO start URL (e.g., https://my-sso-portal.awsapps.com/start)") + if err != nil { + return "", nil, fmt.Errorf("failed to prompt for SSO start URL: %w", err) + } - c.Config.RawCustomConfig.SSOSessions = append(c.Config.RawCustomConfig.SSOSessions, *ssoSession) + region, err = c.Prompter.PromptForRegion("us-east-1") + if err != nil { + return "", nil, fmt.Errorf("failed to prompt for SSO region: %w", err) + } + + ssoSession := &models.SSOSession{ + Name: name, + StartURL: strings.TrimSuffix(startURL, "#"), + Region: region, + Scopes: "sso:account:access", } + c.Config.RawCustomConfig.SSOSessions = append(c.Config.RawCustomConfig.SSOSessions, *ssoSession) return configPath, ssoSession, nil } -func (c *RealSSOClient) selectSSOSession() (*models.SSOSession, error) { +func (c *RealSSOClient) SelectSSOSession() (*models.SSOSession, error) { options := make([]string, 0, len(c.Config.RawCustomConfig.SSOSessions)+1) sessionMap := make(map[string]*models.SSOSession) for i := range c.Config.RawCustomConfig.SSOSessions { @@ -123,7 +129,7 @@ func (c *RealSSOClient) selectSSOSession() (*models.SSOSession, error) { return ssoSession, nil } -func (c *RealSSOClient) configureSSOSession(sessionName, startURL, region, scopes string) error { +func (c *RealSSOClient) ConfigureSSOSession(sessionName, startURL, region, scopes string) error { fmt.Println("\nConfiguring AWS SSO session in ~/.aws/config...") startURL = strings.TrimSuffix(startURL, "#") @@ -225,7 +231,7 @@ func (c *RealSSOClient) configureSSOSession(sessionName, startURL, region, scope return nil } -func (c *RealSSOClient) runSSOLogin(sessionName string) error { +func (c *RealSSOClient) RunSSOLogin(sessionName string) error { if err := c.validateAWSConfig(sessionName); err != nil { return fmt.Errorf("invalid SSO configuration: %w", err) } @@ -259,7 +265,7 @@ func (c *RealSSOClient) validateAWSConfig(sessionName string) error { return nil } -func (c *RealSSOClient) getAccessToken(startURL string) (string, error) { +func (c *RealSSOClient) GetAccessToken(startURL string) (string, error) { startURL = strings.TrimSuffix(startURL, "#") homeDir, err := os.UserHomeDir() if err != nil { diff --git a/internal/sso/session_test.go b/internal/sso/session_test.go index 95cbc7c..c97bc2a 100644 --- a/internal/sso/session_test.go +++ b/internal/sso/session_test.go @@ -1,4 +1,4 @@ -package sso +package sso_test import ( "errors" @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/BerryBytes/awsctl/internal/sso" "github.com/BerryBytes/awsctl/internal/sso/config" "github.com/BerryBytes/awsctl/models" mock_awsctl "github.com/BerryBytes/awsctl/tests/mock" @@ -29,6 +30,9 @@ func TestLoadOrCreateSession(t *testing.T) { tests := []struct { name string initialConfig *models.Config + nameParam string + startURLParam string + regionParam string mockPrompts []mockPrompt wantSession *models.SSOSession wantConfigPath string @@ -36,7 +40,22 @@ func TestLoadOrCreateSession(t *testing.T) { errContains string }{ { - name: "Create new session successfully", + name: "Create new session with parameters", + initialConfig: &models.Config{ + SSOSessions: []models.SSOSession{}, + }, + nameParam: "test-session", + startURLParam: "https://test.awsapps.com/start", + regionParam: "us-west-2", + wantSession: &models.SSOSession{ + Name: "test-session", + StartURL: "https://test.awsapps.com/start", + Region: "us-west-2", + Scopes: "sso:account:access", + }, + }, + { + name: "Create new session interactively", initialConfig: &models.Config{ SSOSessions: []models.SSOSession{}, }, @@ -51,9 +70,7 @@ func TestLoadOrCreateSession(t *testing.T) { Region: "us-west-2", Scopes: "sso:account:access", }, - wantConfigPath: "", }, - { name: "Region prompt error", initialConfig: &models.Config{ @@ -76,7 +93,6 @@ func TestLoadOrCreateSession(t *testing.T) { mockPrompter := mock_sso.NewMockPrompter(ctrl) - // Set up expected mock calls for _, mp := range tt.mockPrompts { switch mp.method { case "PromptWithDefault": @@ -94,14 +110,14 @@ func TestLoadOrCreateSession(t *testing.T) { } } - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Config: config.Config{ RawCustomConfig: tt.initialConfig, }, } - configPath, session, err := client.loadOrCreateSession() + configPath, session, err := client.LoadOrCreateSession(tt.nameParam, tt.startURLParam, tt.regionParam) if tt.wantErr { assert.Error(t, err) @@ -124,7 +140,6 @@ func TestLoadOrCreateSession(t *testing.T) { }) } } - func TestSelectSSOSession(t *testing.T) { tests := []struct { name string @@ -223,14 +238,14 @@ func TestSelectSSOSession(t *testing.T) { } } - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Prompter: mockPrompter, Config: config.Config{ RawCustomConfig: tt.initialConfig, }, } - session, err := client.selectSSOSession() + session, err := client.SelectSSOSession() if tt.wantErr { assert.Error(t, err) @@ -358,11 +373,11 @@ sso_region = us-west-2 tt.mockExec(mockExecutor) } - client := &RealSSOClient{ + client := &sso.RealSSOClient{ Executor: mockExecutor, } - err := client.runSSOLogin(tt.sessionName) + err := client.RunSSOLogin(tt.sessionName) if tt.wantErr { assert.Error(t, err) @@ -460,9 +475,9 @@ func TestGetAccessToken(t *testing.T) { } }() - client := &RealSSOClient{} + client := &sso.RealSSOClient{} - token, err := client.getAccessToken(tt.startURL) + token, err := client.GetAccessToken(tt.startURL) if tt.wantErr { assert.Error(t, err) @@ -567,9 +582,9 @@ sso_registration_scopes = sso:account:access } }() - client := &RealSSOClient{} + client := &sso.RealSSOClient{} - err := client.configureSSOSession(tt.sessionName, tt.startURL, tt.region, tt.scopes) + err := client.ConfigureSSOSession(tt.sessionName, tt.startURL, tt.region, tt.scopes) if tt.wantErr { assert.Error(t, err) diff --git a/internal/sso/sso.go b/internal/sso/sso.go index 63c1def..59e17fb 100644 --- a/internal/sso/sso.go +++ b/internal/sso/sso.go @@ -8,11 +8,11 @@ import ( promptUtils "github.com/BerryBytes/awsctl/utils/prompt" ) -func (c *RealSSOClient) SetupSSO() error { +func (c *RealSSOClient) SetupSSO(opts SSOFlagOptions) error { fmt.Println("AWS SSO Configuration Tool") fmt.Println("-------------------------") - _, ssoSession, err := c.loadOrCreateSession() + _, ssoSession, err := c.LoadOrCreateSession(opts.Name, opts.StartURL, opts.Region) if err != nil { if errors.Is(err, promptUtils.ErrInterrupted) { return nil @@ -20,11 +20,11 @@ func (c *RealSSOClient) SetupSSO() error { return fmt.Errorf("failed to load or create session: %w", err) } - if err := c.configureSSOSession(ssoSession.Name, ssoSession.StartURL, ssoSession.Region, ssoSession.Scopes); err != nil { + if err := c.ConfigureSSOSession(ssoSession.Name, ssoSession.StartURL, ssoSession.Region, ssoSession.Scopes); err != nil { return fmt.Errorf("failed to configure SSO session: %w", err) } - if err := c.runSSOLogin(ssoSession.Name); err != nil { + if err := c.RunSSOLogin(ssoSession.Name); err != nil { return fmt.Errorf("failed to run SSO login: %w", err) } @@ -46,20 +46,20 @@ func (c *RealSSOClient) SetupSSO() error { profileName := ssoSession.Name + "-profile" - if err := c.configureAWSProfile(profileName, ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { + if err := c.ConfigureAWSProfile(profileName, ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { return fmt.Errorf("failed to configure AWS profile: %w", err) } defaultConfigured := profileName == "default" if !defaultConfigured { - if err := c.configureAWSProfile("default", ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { + if err := c.ConfigureAWSProfile("default", ssoSession.Name, ssoSession.Region, ssoSession.StartURL, accountID, role, ssoSession.Region); err != nil { return fmt.Errorf("failed to configure AWS default profile: %w", err) } defaultConfigured = true } - printSummary(profileName, ssoSession.Name, ssoSession.StartURL, ssoSession.Region, accountID, role, "", "", "") + PrintSummary(profileName, ssoSession.Name, ssoSession.StartURL, ssoSession.Region, accountID, role, "", "", "") fmt.Printf("\nSuccessfully configured AWS profile '%s'!\n", profileName) if defaultConfigured { @@ -81,16 +81,16 @@ func (c *RealSSOClient) InitSSO(refresh, noBrowser bool) error { awsProfile := c.Config.AWSProfile if awsProfile == "" { - if len(profiles) == 0 { - fmt.Println("No profiles found. Configuring SSO...") - if err := c.SetupSSO(); err != nil { - if errors.Is(err, promptUtils.ErrInterrupted) { - return promptUtils.ErrInterrupted - } - return fmt.Errorf("failed to set up SSO: %w", err) - } - return nil - } + // if len(profiles) == 0 { + // fmt.Println("No profiles found. Configuring SSO...") + // if err := c.SetupSSO(); err != nil { + // if errors.Is(err, promptUtils.ErrInterrupted) { + // return promptUtils.ErrInterrupted + // } + // return fmt.Errorf("failed to set up SSO: %w", err) + // } + // return nil + // } awsProfile, err = c.Prompter.SelectFromList("Select AWS profile", profiles) if err != nil { diff --git a/internal/sso/utils.go b/internal/sso/utils.go index 98a0337..96b5a45 100644 --- a/internal/sso/utils.go +++ b/internal/sso/utils.go @@ -7,21 +7,21 @@ import ( "strings" ) -func validateAccountID(accountID string) error { +func ValidateAccountID(accountID string) error { if len(accountID) != 12 || !regexp.MustCompile(`^\d{12}$`).MatchString(accountID) { return fmt.Errorf("invalid account ID: %s (must be 12 digits)", accountID) } return nil } -func validateStartURL(startURL string) error { +func ValidateStartURL(startURL string) error { if !strings.HasPrefix(startURL, "https://") { return fmt.Errorf("invalid start URL: %s (must start with https://)", startURL) } return nil } -func printSummary(profileName, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration string) { +func PrintSummary(profileName, sessionName, ssoStartURL, ssoRegion, accountID, roleName, accountName, roleARN, expiration string) { fmt.Println("\nAWS SSO Configuration Summary:") fmt.Printf("Profile Name: %s\n", profileName) fmt.Printf("SSO Session: %s\n", sessionName) @@ -41,7 +41,7 @@ func printSummary(profileName, sessionName, ssoStartURL, ssoRegion, accountID, r } func (c *RealSSOClient) listSSOAccounts(region, startURL string) ([]string, error) { - accessToken, err := c.getAccessToken(startURL) + accessToken, err := c.GetAccessToken(startURL) if err != nil { return nil, fmt.Errorf("failed to get access token: %w", err) } @@ -91,7 +91,7 @@ func (c *RealSSOClient) listSSOAccounts(region, startURL string) ([]string, erro } func (c *RealSSOClient) listSSORoles(region, startURL, accountID string) ([]string, error) { - accessToken, err := c.getAccessToken(startURL) + accessToken, err := c.GetAccessToken(startURL) if err != nil { return nil, fmt.Errorf("failed to get access token: %w", err) } @@ -146,7 +146,7 @@ func (c *RealSSOClient) selectAccount(region, startURL string) (string, error) { } accountID := strings.SplitN(selectedAccount, " ", 2)[0] - if err := validateAccountID(accountID); err != nil { + if err := ValidateAccountID(accountID); err != nil { return "", err } return accountID, nil diff --git a/internal/sso/utils_test.go b/internal/sso/utils_test.go index 3d39def..dee6e92 100644 --- a/internal/sso/utils_test.go +++ b/internal/sso/utils_test.go @@ -1,8 +1,9 @@ -package sso +package sso_test import ( "testing" + "github.com/BerryBytes/awsctl/internal/sso" "github.com/stretchr/testify/assert" ) @@ -40,7 +41,7 @@ func TestValidateAccountID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateAccountID(tt.accountID) + err := sso.ValidateAccountID(tt.accountID) if tt.expectError { assert.Error(t, err) if tt.errorContains != "" { @@ -87,7 +88,7 @@ func TestValidateStartURL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateStartURL(tt.startURL) + err := sso.ValidateStartURL(tt.startURL) if tt.expectError { assert.Error(t, err) if tt.errorContains != "" { @@ -102,7 +103,7 @@ func TestValidateStartURL(t *testing.T) { func TestPrintSummary(t *testing.T) { t.Run("prints complete summary", func(t *testing.T) { - printSummary( + sso.PrintSummary( "my-profile", "my-session", "https://example.awsapps.com/start", @@ -116,7 +117,7 @@ func TestPrintSummary(t *testing.T) { }) t.Run("prints minimal summary", func(t *testing.T) { - printSummary( + sso.PrintSummary( "my-profile", "my-session", "https://example.awsapps.com/start", diff --git a/tests/mock/sso/sso.go b/tests/mock/sso/sso.go index bf48387..3be720d 100644 --- a/tests/mock/sso/sso.go +++ b/tests/mock/sso/sso.go @@ -8,6 +8,7 @@ import ( reflect "reflect" time "time" + sso "github.com/BerryBytes/awsctl/internal/sso" models "github.com/BerryBytes/awsctl/models" gomock "github.com/golang/mock/gomock" ) @@ -212,17 +213,17 @@ func (mr *MockSSOClientMockRecorder) SSOLogin(awsProfile, refresh, noBrowser int } // SetupSSO mocks base method. -func (m *MockSSOClient) SetupSSO() error { +func (m *MockSSOClient) SetupSSO(opts sso.SSOFlagOptions) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetupSSO") + ret := m.ctrl.Call(m, "SetupSSO", opts) ret0, _ := ret[0].(error) return ret0 } // SetupSSO indicates an expected call of SetupSSO. -func (mr *MockSSOClientMockRecorder) SetupSSO() *gomock.Call { +func (mr *MockSSOClientMockRecorder) SetupSSO(opts interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetupSSO", reflect.TypeOf((*MockSSOClient)(nil).SetupSSO)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetupSSO", reflect.TypeOf((*MockSSOClient)(nil).SetupSSO), opts) } // ValidProfiles mocks base method. diff --git a/utils/general/general.go b/utils/general/general.go index 30192f9..29be5dd 100644 --- a/utils/general/general.go +++ b/utils/general/general.go @@ -115,3 +115,9 @@ func IsRegionValid(region string) bool { return isValidRegionFormat(region) } + +var validNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_]{0,126}[a-zA-Z0-9]$`) + +func IsValidSessionName(name string) bool { + return validNameRegex.MatchString(name) +} From c173278c14e479998c0acb782ffeb340f141147f Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 15:43:08 +0545 Subject: [PATCH 40/51] fix: removed the sso init to trigger sso setup function when no profiles found --- internal/sso/sso.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/internal/sso/sso.go b/internal/sso/sso.go index 59e17fb..f1b708b 100644 --- a/internal/sso/sso.go +++ b/internal/sso/sso.go @@ -81,16 +81,13 @@ func (c *RealSSOClient) InitSSO(refresh, noBrowser bool) error { awsProfile := c.Config.AWSProfile if awsProfile == "" { - // if len(profiles) == 0 { - // fmt.Println("No profiles found. Configuring SSO...") - // if err := c.SetupSSO(); err != nil { - // if errors.Is(err, promptUtils.ErrInterrupted) { - // return promptUtils.ErrInterrupted - // } - // return fmt.Errorf("failed to set up SSO: %w", err) - // } - // return nil - // } + if len(profiles) == 0 { + fmt.Println("No AWS SSO profiles found.") + fmt.Println("Run `awsctl sso setup` to create a new profile.") + fmt.Println("Run `awsctl sso setup -h` for help.") + var ErrNoProfiles = errors.New("no AWS SSO profiles found") + return ErrNoProfiles + } awsProfile, err = c.Prompter.SelectFromList("Select AWS profile", profiles) if err != nil { From e75b530fa347470eb2d50e387f674e7abd7240af Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 15:55:27 +0545 Subject: [PATCH 41/51] docs: changed readme and command.md with the added flags on the setup command --- README.md | 16 +++++------ docs/usage/commands.md | 60 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5637132..2b7f9ea 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,14 @@ ssoSessions: The following table summarizes the available `awsctl` commands: -| Command | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `awsctl sso setup` | Creates or updates an AWS SSO profile. If a config file is available at `~/.config/awsctl/`, it will be used; otherwise, you will be prompted to enter the SSO Start URL and Region. The selected profile is then set as the default and authenticated. | -| `awsctl sso init` | Starts SSO authentication by allowing you to select from existing AWS SSO profiles (created via `awsctl sso setup`). Useful for switching between multiple configured SSO profiles. | -| `awsctl bastion` | Manages SSH/SSM connections, SOCKS proxy, or port forwarding to bastion hosts or EC2 instances. | -| `awsctl rds` | Connects to RDS databases directly or via SSH/SSM tunnels. | -| `awsctl eks` | Updates kubeconfig for accessing Amazon EKS clusters. | -| `awsctl ecr` | Authenticates to Amazon ECR for container image operations. | +| Command | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `awsctl sso setup` | Creates/updates AWS SSO profiles. Supports flags: `--name`, `--start-url`, `--region` for non-interactive setup. Uses `~/.config/awsctl/config.yml` if available; otherwise, you will be prompted to enter the SSO Start URL, Region and SSO Name. The selected profile is then set as the default and authenticated. | +| `awsctl sso init` | Starts SSO authentication by allowing you to select from existing AWS SSO profiles (created via `awsctl sso setup`). Useful for switching between multiple configured SSO profiles. | +| `awsctl bastion` | Manages SSH/SSM connections, SOCKS proxy, or port forwarding to bastion hosts or EC2 instances. | +| `awsctl rds` | Connects to RDS databases directly or via SSH/SSM tunnels. | +| `awsctl eks` | Updates kubeconfig for accessing Amazon EKS clusters. | +| `awsctl ecr` | Authenticates to Amazon ECR for container image operations. | #### For detailed CLI command usage, see [Command Usage Documentation](docs/usage/commands.md). diff --git a/docs/usage/commands.md b/docs/usage/commands.md index 08d651b..c851a8b 100644 --- a/docs/usage/commands.md +++ b/docs/usage/commands.md @@ -6,11 +6,61 @@ Creates or updates AWS SSO profiles. -- Prompts for: - - SSO Start URL - - AWS Region -- Uses defaults from `~/.config/awsctl/config.yml` if available. -- Automatically sets the profile as default and authenticates it after setup. +#### Basic Usage + +```bash +awsctl sso setup [flags] +``` + +#### Flags + +| Flag | Description | Required | Example | +| ------------- | ---------------------------------------------- | -------- | ---------------------------------------------- | +| `--name` | SSO session name | No | `--name my-sso-session` | +| `--start-url` | AWS SSO start URL (must begin with `https://`) | No | `--start-url https://my-sso.awsapps.com/start` | +| `--region` | AWS region for the SSO session | No | `--region us-east-1` | + +#### Behavior + +- **Interactive Mode** (default when no flags): + + - Prompts for: + - SSO Start URL + - AWS Region + - Session name (default: "default-sso") + - Uses defaults from `~/.config/awsctl/config.yml` if available + - Validates all inputs before creating session + +- **Non-interactive Mode** (when all flags provided): + + - Creates session immediately without prompts + - Validates: + - Start URL format (`https://`) + - Valid AWS region + - Proper session name format + +#### Examples + +1. Fully interactive: + +```bash +awsctl sso setup +``` + +2. Fully non-interactive: + +```bash +awsctl sso setup --name dev-session --start-url https://dev.awsapps.com/start --region us-east-1 +``` + +#### Validation Rules + +- `--start-url`: Must begin with `https://` +- `--region`: Must be valid AWS region code +- `--name`: + - Alphanumeric with dashes/underscores + - Cannot start/end with special chars + - 3-64 characters --- From fde19c3ac3455c54585c7a71a4ac84fbe597e9b2 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:05:23 +0545 Subject: [PATCH 42/51] chore: commit for next release [release] From 8b2144e2aa3eb65ebe574df0b1eabd5a620961ef Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:18:23 +0545 Subject: [PATCH 43/51] chore: changed internal release workflow --- .github/workflows/releaser-internal.yaml | 2 +- .goreleaser-internal.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index 75e3ebe..ed5861d 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -56,7 +56,7 @@ jobs: VERSION=$(echo "$CURRENT_TAG" | sed 's/^v//' | sed 's/-beta-internal//') NEW_VERSION=$(semver "$VERSION" -i patch) - NEW_TAG="v${NEW_VERSION}" + NEW_TAG="v${NEW_VERSION}-beta-internal" echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT echo "New tag: $NEW_TAG" diff --git a/.goreleaser-internal.yaml b/.goreleaser-internal.yaml index 950ea51..db5154f 100644 --- a/.goreleaser-internal.yaml +++ b/.goreleaser-internal.yaml @@ -24,7 +24,6 @@ release: github: owner: berrybytes name: awsctl - name_template: "{{.Version}}-beta-internal" extra_files: - glob: ./LICENSE - glob: ./README.md From ca6959bf2b607f45932b4c4d0f6bb7b7b6246c47 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:25:02 +0545 Subject: [PATCH 44/51] chore: commit for next release [release] From d9024035f515b11267200b77a49d0cd13401d8c9 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:34:42 +0545 Subject: [PATCH 45/51] chore: commit for next release [release] From de4f9afa72fa7f9a65723ff8574d06baa34cb0e8 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:39:07 +0545 Subject: [PATCH 46/51] chore: commit for next release [release] From 280f696bf8b882b0efc86fa1d6cb9fdf7616eb25 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Tue, 8 Jul 2025 16:41:10 +0545 Subject: [PATCH 47/51] chore: test [release] --- .github/workflows/releaser-internal.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/releaser-internal.yaml b/.github/workflows/releaser-internal.yaml index ed5861d..ee8531d 100644 --- a/.github/workflows/releaser-internal.yaml +++ b/.github/workflows/releaser-internal.yaml @@ -4,8 +4,8 @@ on: push: branches: - develop - paths-ignore: - - "**.md" + # paths-ignore: + # - "**.md" workflow_dispatch: # concurrency: From 86bc9bcc457694b9a1cd856efa977fac5f0462ef Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 9 Jul 2025 14:51:07 +0545 Subject: [PATCH 48/51] docs: changed the local installation script to fetch the pre-release version --- install-awsctl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-awsctl.sh b/install-awsctl.sh index 6ba1c01..cb0c829 100755 --- a/install-awsctl.sh +++ b/install-awsctl.sh @@ -8,7 +8,7 @@ BUILD_DIR="$(pwd)/bin" INSTALL_DIR="$HOME/awsctl" GC_FLAGS="all=-N -l" -VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.2.0") +VERSION=$(git tag --sort=-v:refname | head -n 1 | sed 's/^v//' || echo "0.2.0") LD_FLAGS="-X 'main.Version=$VERSION' -s -w" # Detect shell detect_shell() { From 6f2348a8a0ccce65c18a4784daf87a1e919a08f1 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Wed, 9 Jul 2025 16:20:13 +0545 Subject: [PATCH 49/51] chore: changed the command usage doc --- docs/usage/commands.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/usage/commands.md b/docs/usage/commands.md index c851a8b..2929a90 100644 --- a/docs/usage/commands.md +++ b/docs/usage/commands.md @@ -14,11 +14,11 @@ awsctl sso setup [flags] #### Flags -| Flag | Description | Required | Example | -| ------------- | ---------------------------------------------- | -------- | ---------------------------------------------- | -| `--name` | SSO session name | No | `--name my-sso-session` | -| `--start-url` | AWS SSO start URL (must begin with `https://`) | No | `--start-url https://my-sso.awsapps.com/start` | -| `--region` | AWS region for the SSO session | No | `--region us-east-1` | +| Flag | Description | Example | +| ------------- | ---------------------------------------------- | ---------------------------------------------- | +| `--name` | SSO session name | `--name my-sso-session` | +| `--start-url` | AWS SSO start URL (must begin with `https://`) | `--start-url https://my-sso.awsapps.com/start` | +| `--region` | AWS region for the SSO session | `--region us-east-1` | #### Behavior From 9c863149b7007b6d5d6e37a4c499f46a4d376a5e Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Thu, 10 Jul 2025 14:10:11 +0545 Subject: [PATCH 50/51] chore: changed doc for release --- CODE_OF_CONDUCT.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 4 ++- README.md | 8 ++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f0670a3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We, as contributors and maintainers of awsctl, pledge to foster an open, inclusive, and harassment-free environment for everyone. Participation in our project and community is a respectful and welcoming experience, regardless of: + +- Age +- Background +- Disability +- Ethnicity +- Gender identity/expression +- Experience level +- Nationality +- Personal appearance +- Race +- Religion +- Technology choices + +## Our Standards + +### Encouraged Behaviors + +- **Collaboration**: Welcoming language and constructive feedback +- **Empathy**: Respect differing perspectives and experiences +- **Focus**: Prioritize solutions that benefit AWS users and the community + +### Unacceptable Behaviors + +- Harassment, trolling, or derogatory comments +- Sexualized language/unwelcome advances +- Doxing (sharing private info without consent) +- Any behavior undermining a professional environment + +## Project Maintainer Responsibilities + +Maintainers will: + +1. Clarify and enforce these standards +2. Address violations promptly and fairly +3. Remove harmful content or contributors if needed + +## Scope + +Applies to all project spaces: + +- GitHub repositories, issues, PRs +- Community forums (e.g., Discord, Slack) +- Interactions when representing awsctl (e.g., conferences) + +### **Enforcement Guidelines** + +1. **First Instance:** + + - The message is edited and marked as "abuse." + - Content added: + > "Dear user, we want AWSCTL to provide a welcoming and respectful environment. Your [comment/issue/PR] has been reported and marked as abuse according to our [Code of Conduct](./CODE_OF_CONDUCT.md). Thank you." + +2. **Second Instance:** + + - The message is again edited and marked as "abuse." + - Results in a **7-day ban**. + +3. **Third Instance:** + - The message is edited and marked as "abuse." + - Results in a **permanent ban**. + +All enforcement actions are discussed among maintainers beforehand to ensure fairness. Reports must be resolved following the [GitHub Reporting Guidelines](https://docs.github.com/en/communities/moderating-comments-and-conversations/managing-reported-content-in-your-organizations-repository#resolving-a-report). + +## Attribution + +Adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4. + +--- + +By participating in this project, you agree to abide by its terms. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9aee22e..cda57ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,5 +34,7 @@ Your contributions help make awsctl better for everyone. We appreciate your supp ### Contributors - [Mousam Dahal](https://github.com/mousamdahal) -- [Sanjog Llama](https://github.com/sanjog-lama) +- [Sanjog Lama](https://github.com/sanjog-lama) +- [Sanjog Lama](https://github.com/sawnjordan) - [Konika Sitoula](https://github.com/konika-sitoula) +- [Umesh Khatiwada](https://github.com/umesh-khatiwada) diff --git a/README.md b/README.md index 2b7f9ea..1ac7399 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,12 @@ The following table summarizes the available `awsctl` commands: We welcome contributions! Please see our [contributing guidelines](CONTRIBUTING.md) for more details. +--- + +## 👥 Community + +Join our vibrant [Discord community](https://discord.gg/g4a3P9af) to connect with contributors and maintainers. Engage in meaningful discussions, collaborate on ideas, and stay updated on the latest developments! + ### Code Review Automation This project uses [Coderabbit AI](https://www.coderabbit.ai/) to assist with pull request reviews. @@ -185,6 +191,6 @@ Special thanks to [Berrybytes](https://www.berrybytes.com) for bringing this pro ## License -AWS CLI Tools is open-source software licensed under the [MIT License](LICENSE). +AWSCTL CLI Tools is open-source software licensed under the [MIT License](LICENSE). This revised README is more visually appealing and user-friendly while maintaining its clarity and professionalism. From 10291eaf8a125db89712a7dd13f3db6757e29972 Mon Sep 17 00:00:00 2001 From: sawnjordan Date: Fri, 11 Jul 2025 13:08:53 +0545 Subject: [PATCH 51/51] docs: changed the installation URL on install.sh script to main branch --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ac7399..1ff87ec 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,13 @@ AWS CLI leverages the powerful [Cobra](https://github.com/spf13/cobra) framework 1. To install the latest version. ``` -curl -sS https://raw.githubusercontent.com/berrybytes/awsctl/develop/installer.sh | bash +curl -sS https://raw.githubusercontent.com/berrybytes/awsctl/main/installer.sh | bash ``` 2. To install specific version (e.g: `v0.0.1`) ``` -curl -sS https://raw.githubusercontent.com/berrybytes/awsctl/develop/installer.sh | bash -s -- v0.0.1 +curl -sS https://raw.githubusercontent.com/berrybytes/awsctl/main/installer.sh | bash -s -- v0.0.1 ``` ### Manual