From 40081dd28389797e9c20cc82138e92edcd63a029 Mon Sep 17 00:00:00 2001 From: pk556 Date: Thu, 19 Dec 2024 14:48:17 -0500 Subject: [PATCH 1/6] add route --- api/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/routes.go b/api/routes.go index 6598a57..e38c525 100644 --- a/api/routes.go +++ b/api/routes.go @@ -72,6 +72,7 @@ func (s *server) routes() { api.HandleFunc("/{account}/instances/{id}/power", s.InstanceStateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/ssm/command", s.InstanceSendCommandHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/ssm/association", s.InstanceSSMAssociationHandler).Methods(http.MethodPut) + api.HandleFunc("/{account}/sgs/ssm/association", s.SecurityGroupSSMAssociationHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/tags", s.InstanceUpdateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/attribute", s.InstanceUpdateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupUpdateHandler).Methods(http.MethodPut) From 0ab7cb86853ff7aa5fdfb17d3d5f455f5275a509 Mon Sep 17 00:00:00 2001 From: pk556 Date: Thu, 19 Dec 2024 14:48:39 -0500 Subject: [PATCH 2/6] add handler and service function --- api/handlers_sgs.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ api/types.go | 4 ++- ssm/association.go | 30 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/api/handlers_sgs.go b/api/handlers_sgs.go index 76f26d3..4ee9477 100644 --- a/api/handlers_sgs.go +++ b/api/handlers_sgs.go @@ -8,6 +8,7 @@ import ( "github.com/YaleSpinup/apierror" "github.com/YaleSpinup/ec2-api/ec2" + "github.com/YaleSpinup/ec2-api/ssm" "github.com/gorilla/mux" ) @@ -227,3 +228,69 @@ func (s *server) SecurityGroupDeleteHandler(w http.ResponseWriter, r *http.Reque handleResponseOk(w, "OK") } + +func (s *server) SecurityGroupSSMAssociationHandler(w http.ResponseWriter, r *http.Request) { + w = LogWriter{w} + vars := mux.Vars(r) + account := s.mapAccountNumber(vars["account"]) + + req := &SSMAssociationRequest{} + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + msg := fmt.Sprintf("cannot decode body into ssm create input %s", err) + handleError(w, apierror.New(apierror.ErrBadRequest, msg, err)) + return + } + + // Check for missing values in request body + requiredFields := map[string]string{ + "Document": req.Document, + "TagKey": req.TagKey, + } + for field, value := range requiredFields { + if value == "" { + errMsg := fmt.Sprintf("%s is mandatory", field) + handleError(w, apierror.New(apierror.ErrBadRequest, errMsg, nil)) + return + } + } + // Check if TagValues is nil or empty + if len(req.TagValues) == 0 { + errMsg := "TagValues is mandatory" + handleError(w, apierror.New(apierror.ErrBadRequest, errMsg, nil)) + return + } + + // Assume role in account + role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName) + policy, err := ssmAssociationPolicy() + if err != nil { + handleError(w, err) + return + } + + session, err := s.assumeRole( + r.Context(), + s.session.ExternalID, + role, + policy, + "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess", + "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", + ) + if err != nil { + msg := fmt.Sprintf("failed to assume role in account: %s", account) + handleError(w, apierror.New(apierror.ErrForbidden, msg, err)) + return + } + + service := ssm.New( + ssm.WithSession(session.Session), + ) + + out, err := service.CreateAssociationByTag(r.Context(), req.TagKey, req.TagValues, req.Document) + if err != nil { + handleError(w, err) + return + } + + handleResponseOk(w, out) +} diff --git a/api/types.go b/api/types.go index 0654fc2..06f49a3 100644 --- a/api/types.go +++ b/api/types.go @@ -805,7 +805,9 @@ type Ec2InstanceStateChangeRequest struct { } type SSMAssociationRequest struct { - Document string `json:"document"` + Document string `json:"document"` + TagKey string `json:"tagKey"` + TagValues []string `json:"tagValues"` } type SsmCommandRequest struct { diff --git a/ssm/association.go b/ssm/association.go index 251a563..b71d845 100644 --- a/ssm/association.go +++ b/ssm/association.go @@ -40,3 +40,33 @@ func (s *SSM) CreateAssociation(ctx context.Context, instanceId, docName string) log.Debugf("got output creating SSM Association: %+v", out) return aws.StringValue(out.AssociationDescription.AssociationId), nil } + +func (s *SSM) CreateAssociationByTag(ctx context.Context, tagKey string, tagValues []string, docName string) (string, error) { + // Check for missing values + if tagKey == "" || tagValues == nil { + return "", apierror.New(apierror.ErrBadRequest, "both tagKey and tagValues should be present", nil) + } + if docName == "" { + return "", apierror.New(apierror.ErrBadRequest, "docName should be present", nil) + } + + // Create the Targets structure + targets := []*ssm.Target{ + { + Key: aws.String(tagKey), + Values: aws.StringSlice(tagValues), + }, + } + + inp := &ssm.CreateAssociationInput{ + Name: aws.String(docName), + Targets: targets, + } + + out, err := s.Service.CreateAssociationWithContext(ctx, inp) + if err != nil { + return "", common.ErrCode("failed to create association", err) + } + log.Debugf("got output creating SSM Association: %+v", out) + return aws.StringValue(out.AssociationDescription.AssociationId), nil +} From 4977efae9b065e87bc8b955d6740f2d991952d27 Mon Sep 17 00:00:00 2001 From: pk556 Date: Mon, 23 Dec 2024 13:27:51 -0500 Subject: [PATCH 3/6] bug to specify tag key --- .gitignore | 4 +++- ssm/association.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8930d3d..e532369 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ vendor .vscode debug .idea/ -*.exe \ No newline at end of file +*.exe +# Fresh +tmp/runner-build \ No newline at end of file diff --git a/ssm/association.go b/ssm/association.go index b71d845..b4f3cac 100644 --- a/ssm/association.go +++ b/ssm/association.go @@ -53,7 +53,7 @@ func (s *SSM) CreateAssociationByTag(ctx context.Context, tagKey string, tagValu // Create the Targets structure targets := []*ssm.Target{ { - Key: aws.String(tagKey), + Key: aws.String("tag:" + tagKey), Values: aws.StringSlice(tagValues), }, } From afcd289feb7df35150b197ce07fd1ab5e262ee7d Mon Sep 17 00:00:00 2001 From: pk556 Date: Fri, 10 Jan 2025 14:08:27 -0500 Subject: [PATCH 4/6] change route to a post route --- api/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/routes.go b/api/routes.go index e38c525..0daeaef 100644 --- a/api/routes.go +++ b/api/routes.go @@ -63,6 +63,7 @@ func (s *server) routes() { api.HandleFunc("/{account}/instances", s.InstanceCreateHandler).Methods(http.MethodPost) api.HandleFunc("/{account}/instances/{id}/volumes", s.VolumeAttachHandler).Methods(http.MethodPost) api.HandleFunc("/{account}/sgs", s.SecurityGroupCreateHandler).Methods(http.MethodPost) + api.HandleFunc("/{account}/ssm/association", s.SSMAssociationByTagHandler).Methods(http.MethodPost) api.HandleFunc("/{account}/volumes", s.VolumeCreateHandler).Methods(http.MethodPost) api.HandleFunc("/{account}/snapshots", s.SnapshotCreateHandler).Methods(http.MethodPost) api.HandleFunc("/{account}/images", s.ImageCreateHandler).Methods(http.MethodPost) @@ -72,7 +73,6 @@ func (s *server) routes() { api.HandleFunc("/{account}/instances/{id}/power", s.InstanceStateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/ssm/command", s.InstanceSendCommandHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/ssm/association", s.InstanceSSMAssociationHandler).Methods(http.MethodPut) - api.HandleFunc("/{account}/sgs/ssm/association", s.SecurityGroupSSMAssociationHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/tags", s.InstanceUpdateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/instances/{id}/attribute", s.InstanceUpdateHandler).Methods(http.MethodPut) api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupUpdateHandler).Methods(http.MethodPut) From 02485ed773e6c22bd182c9ac04c7e14afaa61f6c Mon Sep 17 00:00:00 2001 From: pk556 Date: Fri, 10 Jan 2025 14:08:53 -0500 Subject: [PATCH 5/6] update logic to handle multiple tags and other fields --- api/handlers_sgs.go | 27 ++++++++++----------- api/types.go | 15 ++++++++---- ssm/association.go | 58 ++++++++++++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/api/handlers_sgs.go b/api/handlers_sgs.go index 4ee9477..64403a9 100644 --- a/api/handlers_sgs.go +++ b/api/handlers_sgs.go @@ -229,12 +229,12 @@ func (s *server) SecurityGroupDeleteHandler(w http.ResponseWriter, r *http.Reque handleResponseOk(w, "OK") } -func (s *server) SecurityGroupSSMAssociationHandler(w http.ResponseWriter, r *http.Request) { +func (s *server) SSMAssociationByTagHandler(w http.ResponseWriter, r *http.Request) { w = LogWriter{w} vars := mux.Vars(r) account := s.mapAccountNumber(vars["account"]) - req := &SSMAssociationRequest{} + req := &SSMAssociationByTagRequest{} if err := json.NewDecoder(r.Body).Decode(req); err != nil { msg := fmt.Sprintf("cannot decode body into ssm create input %s", err) handleError(w, apierror.New(apierror.ErrBadRequest, msg, err)) @@ -242,20 +242,19 @@ func (s *server) SecurityGroupSSMAssociationHandler(w http.ResponseWriter, r *ht } // Check for missing values in request body - requiredFields := map[string]string{ - "Document": req.Document, - "TagKey": req.TagKey, + errMsg := "" + if req.Document == "" { + errMsg = fmt.Sprintf("document is mandatory") } - for field, value := range requiredFields { - if value == "" { - errMsg := fmt.Sprintf("%s is mandatory", field) - handleError(w, apierror.New(apierror.ErrBadRequest, errMsg, nil)) - return + if len(req.TagFilters) == 0 { + errMsg = fmt.Sprintf("tagFilters is mandatory") + } + for _, tagValues := range req.TagFilters { + if len(tagValues) == 0 { + errMsg = fmt.Sprintf("You are missing values for one of your tags") } } - // Check if TagValues is nil or empty - if len(req.TagValues) == 0 { - errMsg := "TagValues is mandatory" + if errMsg != "" { handleError(w, apierror.New(apierror.ErrBadRequest, errMsg, nil)) return } @@ -286,7 +285,7 @@ func (s *server) SecurityGroupSSMAssociationHandler(w http.ResponseWriter, r *ht ssm.WithSession(session.Session), ) - out, err := service.CreateAssociationByTag(r.Context(), req.TagKey, req.TagValues, req.Document) + out, err := service.CreateAssociationByTag(r.Context(), req.Name, req.Document, req.DocumentVersion, req.TagFilters, req.Parameters) if err != nil { handleError(w, err) return diff --git a/api/types.go b/api/types.go index 06f49a3..7ae5220 100644 --- a/api/types.go +++ b/api/types.go @@ -2,9 +2,10 @@ package api import ( "encoding/json" - "github.com/aws/aws-sdk-go/service/iam" "time" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/service/ec2" @@ -805,9 +806,15 @@ type Ec2InstanceStateChangeRequest struct { } type SSMAssociationRequest struct { - Document string `json:"document"` - TagKey string `json:"tagKey"` - TagValues []string `json:"tagValues"` + Document string `json:"document"` +} + +type SSMAssociationByTagRequest struct { + Name string `json:"name"` + Document string `json:"document"` + DocumentVersion int `json:"documentVersion"` + TagFilters map[string][]string `json:"tagFilters"` + Parameters map[string][]string `json:"parameters"` } type SsmCommandRequest struct { diff --git a/ssm/association.go b/ssm/association.go index b4f3cac..a0e4b56 100644 --- a/ssm/association.go +++ b/ssm/association.go @@ -2,6 +2,7 @@ package ssm import ( "context" + "strconv" "github.com/YaleSpinup/apierror" "github.com/YaleSpinup/ec2-api/common" @@ -41,29 +42,54 @@ func (s *SSM) CreateAssociation(ctx context.Context, instanceId, docName string) return aws.StringValue(out.AssociationDescription.AssociationId), nil } -func (s *SSM) CreateAssociationByTag(ctx context.Context, tagKey string, tagValues []string, docName string) (string, error) { - // Check for missing values - if tagKey == "" || tagValues == nil { - return "", apierror.New(apierror.ErrBadRequest, "both tagKey and tagValues should be present", nil) - } - if docName == "" { - return "", apierror.New(apierror.ErrBadRequest, "docName should be present", nil) - } - +func (s *SSM) CreateAssociationByTag(ctx context.Context, associationName string, docName string, docVersion int, tagFilters map[string][]string, parameters map[string][]string) (string, error) { // Create the Targets structure - targets := []*ssm.Target{ - { - Key: aws.String("tag:" + tagKey), - Values: aws.StringSlice(tagValues), - }, + targets := []*ssm.Target{} + for tagKey, tagValues := range tagFilters { + if tagKey == "" { + return "", apierror.New(apierror.ErrBadRequest, "tag key cannot be empty", nil) + } + targets = append(targets, + &ssm.Target{ + Key: aws.String("tag:" + tagKey), + Values: aws.StringSlice(tagValues), + }, + ) } - inp := &ssm.CreateAssociationInput{ + // Create the input + input := &ssm.CreateAssociationInput{ Name: aws.String(docName), Targets: targets, } - out, err := s.Service.CreateAssociationWithContext(ctx, inp) + // Optionally add fields if present + // Handle optoinal association name + if associationName != "" { + input.AssociationName = aws.String(associationName) + } + // Handle optional document version + if docVersion != 0 { + input.DocumentVersion = aws.String(strconv.Itoa(docVersion)) + } else { + input.DocumentVersion = aws.String("$DEFAULT") + } + // Handle optional parameters + if len(parameters) > 0 { + // Conver the parameters from map[string][]string to map[string][]*string + awsParams := make(map[string][]*string) + for key, values := range parameters { + // Convert each string in the slice to *string + var awsValues []*string + for _, val := range values { + awsValues = append(awsValues, aws.String(val)) + } + awsParams[key] = awsValues + } + input.Parameters = awsParams + } + + out, err := s.Service.CreateAssociationWithContext(ctx, input) if err != nil { return "", common.ErrCode("failed to create association", err) } From fb8b32bf504b33bbe78ab568b58b0ab958bb2bb7 Mon Sep 17 00:00:00 2001 From: pk556 Date: Mon, 13 Jan 2025 16:02:33 -0500 Subject: [PATCH 6/6] add log to output association id --- ssm/association.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ssm/association.go b/ssm/association.go index a0e4b56..b856611 100644 --- a/ssm/association.go +++ b/ssm/association.go @@ -94,5 +94,6 @@ func (s *SSM) CreateAssociationByTag(ctx context.Context, associationName string return "", common.ErrCode("failed to create association", err) } log.Debugf("got output creating SSM Association: %+v", out) + log.Info("created association with id: ", aws.StringValue(out.AssociationDescription.AssociationId)) return aws.StringValue(out.AssociationDescription.AssociationId), nil }