From 582ecaf0d2a7bee01cc7414ea85b5032c9590063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Cor=C3=A9?= Date: Wed, 28 Jun 2023 10:42:44 +0200 Subject: [PATCH] Add example playbooks (#24) * Fix placement request not entirely saved in DB * Return credentials with GET placements/{uuid} The credentials are needed by anarchy when the placement already exists. Decrypt and return it. * authorize only admins to GET all placements All API endpoints are protected, but as a mesure of security, allow only Admins to list all placements. * Add playbook example to Get a sandbox * Add dedicated function LoadResourcesWithCreds Keep LoadResources() method empty from any credential * Make get.yml idempotent * Add release playbook * Add annotations (guid, envtype, email, owner, ..) * Make release.yml idempotent --- cmd/sandbox-api/handlers.go | 4 +- cmd/sandbox-api/main.go | 2 +- docs/api-reference/swagger.yaml | 1 + docs/examples/ansible/get.yaml | 113 +++++++++++++++++++++++++++++ docs/examples/ansible/release.yaml | 45 ++++++++++++ internal/dynamodb/accounts.go | 42 +++++++++-- internal/models/aws_account.go | 1 + internal/models/placements.go | 17 +++++ todo.org | 3 +- 9 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 docs/examples/ansible/get.yaml create mode 100644 docs/examples/ansible/release.yaml diff --git a/cmd/sandbox-api/handlers.go b/cmd/sandbox-api/handlers.go index f22e5008..ed4a2c70 100644 --- a/cmd/sandbox-api/handlers.go +++ b/cmd/sandbox-api/handlers.go @@ -150,7 +150,7 @@ func (h *BaseHandler) CreatePlacementHandler(w http.ResponseWriter, r *http.Requ Placement: models.Placement{ ServiceUuid: placementRequest.ServiceUuid, Annotations: placementRequest.Annotations, - Request: v1.PlacementRequest{Resources: placementRequest.Resources}, + Request: placementRequest, }, } placement.Resources = resources @@ -255,7 +255,7 @@ func (h *BaseHandler) GetPlacementHandler(w http.ResponseWriter, r *http.Request log.Logger.Error("GetPlacementHandler", "error", err) return } - placement.LoadResources(h.accountProvider) + placement.LoadResourcesWithCreds(h.accountProvider) w.WriteHeader(http.StatusOK) render.Render(w, r, placement) diff --git a/cmd/sandbox-api/main.go b/cmd/sandbox-api/main.go index 4a6bdd27..95ed019c 100644 --- a/cmd/sandbox-api/main.go +++ b/cmd/sandbox-api/main.go @@ -190,7 +190,6 @@ func main() { r.Put("/api/v1/accounts/{kind}/{account}/status", baseHandler.LifeCycleAccountHandler("status")) r.Get("/api/v1/accounts/{kind}/{account}/status", baseHandler.GetStatusAccountHandler) r.Post("/api/v1/placements", baseHandler.CreatePlacementHandler) - r.Get("/api/v1/placements", baseHandler.GetPlacementsHandler) r.Get("/api/v1/placements/{uuid}", baseHandler.GetPlacementHandler) r.Delete("/api/v1/placements/{uuid}", baseHandler.DeletePlacementHandler) }) @@ -208,6 +207,7 @@ func main() { // --------------------------------- // Routes // --------------------------------- + r.Get("/api/v1/placements", baseHandler.GetPlacementsHandler) r.Post("/api/v1/admin/jwt", adminHandler.IssueLoginJWTHandler) r.Get("/api/v1/admin/jwt", baseHandler.GetJWTHandler) r.Put("/api/v1/admin/jwt/{id}/invalidate", baseHandler.InvalidateTokenHandler) diff --git a/docs/api-reference/swagger.yaml b/docs/api-reference/swagger.yaml index 35bccea9..0d5140f5 100644 --- a/docs/api-reference/swagger.yaml +++ b/docs/api-reference/swagger.yaml @@ -94,6 +94,7 @@ paths: summary: Get all placements description: | The placements are returned. + Only Admins are authorized to get all placements. NOTE: the resources are not returned because that would generate too much data and take too much time. To get the information about accounts for a particular placement, query the `/placements/{uuid}` endpoint. responses: diff --git a/docs/examples/ansible/get.yaml b/docs/examples/ansible/get.yaml new file mode 100644 index 00000000..78e70c4a --- /dev/null +++ b/docs/examples/ansible/get.yaml @@ -0,0 +1,113 @@ +--- +# See https://rhpds.github.io/sandbox/api-reference/ + +- name: Get a placement using uuid + hosts: localhost + gather_facts: false + vars: + sandbox_api_url: http://localhost:8080 + tasks: + - name: Ensure needed variables are set + assert: + that: "{{ check.that }}" + fail_msg: "{{ check.msg }}" + loop_control: + loop_var: check + label: "{{ check.msg }}" + loop: + - msg: sandbox_api_login_token must be provided + that: sandbox_api_login_token is defined + - msg: sandbox_api_url must be provided + that: sandbox_api_url is defined + - msg: uuid is not defined + that: uuid is defined + + - name: Login using the JWT login token + uri: + url: "{{ sandbox_api_url }}/api/v1/login" + headers: + Authorization: Bearer {{ sandbox_api_login_token }} + register: r_login + + - name: Save access token + set_fact: + access_token: "{{ r_login.json.access_token }}" + + - name: Check if placement exists + uri: + headers: + Authorization: Bearer {{ access_token }} + url: "{{ sandbox_api_url }}/api/v1/placements/{{ uuid }}" + method: GET + status_code: [200, 404] + register: r_get_placement + + - name: Set placement + set_fact: + placement: "{{ r_get_placement.json }}" + when: r_get_placement.status == 200 + + - when: r_get_placement.status == 404 + block: + - name: Get a placement, book 1 aws sandbox + when: r_get_placement.status == 404 + uri: + headers: + Authorization: Bearer {{ access_token }} + url: "{{ sandbox_api_url }}/api/v1/placements" + method: POST + body_format: json + body: + service_uuid: "{{ uuid }}" + annotations: + guid: "abcde" + env_type: "ocp4-cluster" + owner: "user" + owner_email: "user@example.com" + comment: "Created by Ansible" + resources: + - kind: AwsSandbox + count: 1 + register: r_new_placement + + - name: Save placement + set_fact: + placement: "{{ r_new_placement.json.Placement }}" + + + - set_fact: + sandbox_name: "{{ placement.resources[0].name }}" + sandbox_zone: "{{ placement.resources[0].zone }}" + sandbox_hosted_zone_id: "{{ placement.resources[0].hosted_zone_id }}" + sandbox_account: "{{ placement.resources[0].account_id }}" + sandbox_account_id: "{{ placement.resources[0].account_id }}" + sandbox_aws_access_key_id: >- + {{ (placement.resources[0].credentials + | selectattr('kind', 'equalto', 'aws_iam_key') + | selectattr('name', 'equalto', 'admin-key') + | first + ).get('aws_access_key_id') }} + sandbox_aws_secret_access_key: >- + {{ (placement.resources[0].credentials + | selectattr('kind', 'equalto', 'aws_iam_key') + | selectattr('name', 'equalto', 'admin-key') + | first).get('aws_secret_access_key') }} + + - name: Save secret of aws_sandbox_secrets dictionary + set_fact: + aws_sandbox_secrets: + sandbox_aws_access_key_id: "{{ sandbox_aws_access_key_id }}" + sandbox_aws_secret_access_key: "{{ sandbox_aws_secret_access_key }}" + sandbox_hosted_zone_id: "{{ sandbox_hosted_zone_id }}" + sandbox_name: "{{ sandbox_name }}" + sandbox_account: "{{ sandbox_account }}" + sandbox_account_id: "{{ sandbox_account_id }}" + sandbox_zone: "{{ sandbox_zone }}" + # agnosticd + aws_access_key_id: "{{ sandbox_aws_access_key_id }}" + aws_secret_access_key: "{{ sandbox_aws_secret_access_key }}" + HostedZoneId: "{{ sandbox_hosted_zone_id }}" + subdomain_base_suffix: ".{{ sandbox_zone }}" + + - debug: + var: aws_sandbox_secrets diff --git a/docs/examples/ansible/release.yaml b/docs/examples/ansible/release.yaml new file mode 100644 index 00000000..40d06b20 --- /dev/null +++ b/docs/examples/ansible/release.yaml @@ -0,0 +1,45 @@ +# See https://rhpds.github.io/sandbox/api-reference/ + +- name: Release a placement using uuid + hosts: localhost + gather_facts: false + vars: + sandbox_api_url: http://localhost:8080 + tasks: + - name: Ensure needed variables are set + assert: + that: "{{ check.that }}" + fail_msg: "{{ check.msg }}" + loop_control: + loop_var: check + label: "{{ check.msg }}" + loop: + - msg: sandbox_api_login_token must be provided + that: sandbox_api_login_token is defined + - msg: sandbox_api_url must be provided + that: sandbox_api_url is defined + - msg: uuid is not defined + that: uuid is defined + + - name: Login using the JWT login token + uri: + url: "{{ sandbox_api_url }}/api/v1/login" + headers: + Authorization: Bearer {{ sandbox_api_login_token }} + register: r_login + + - name: Save access token + set_fact: + access_token: "{{ r_login.json.access_token }}" + + - name: Release placement + uri: + headers: + Authorization: Bearer {{ access_token }} + url: "{{ sandbox_api_url }}/api/v1/placements/{{ uuid }}" + method: DELETE + status_code: [200, 404] + register: r_placement + + - debug: + var: r_placement diff --git a/internal/dynamodb/accounts.go b/internal/dynamodb/accounts.go index 20ba73bb..0f40721a 100644 --- a/internal/dynamodb/accounts.go +++ b/internal/dynamodb/accounts.go @@ -148,8 +148,18 @@ func makeAccount(account AwsAccountDynamoDB) models.AwsAccount { return a } -// makeAccount creates new models.AwsAccountWithCreds from AwsAccountDynamoDB -func makeAccountWithCreds(account AwsAccountDynamoDB) models.AwsAccountWithCreds { + +// makeAccounts creates new []models.AwsAccount from []AwsAccountDynamoDB +func makeAccounts(accounts []AwsAccountDynamoDB) []models.AwsAccount { + r := []models.AwsAccount{} + for _, account := range accounts { + r = append(r, makeAccount(account)) + } + + return r +} +// makeAccountWithCreds creates new models.AwsAccountWithCreds from AwsAccountDynamoDB +func (provider *AwsAccountDynamoDBProvider) makeAccountWithCreds(account AwsAccountDynamoDB) models.AwsAccountWithCreds { a := makeAccount(account) @@ -157,11 +167,16 @@ func makeAccountWithCreds(account AwsAccountDynamoDB) models.AwsAccountWithCreds AwsAccount: a, } + decrypted, err := provider.DecryptSecret(account.AwsSecretAccessKey) + if err != nil { + decrypted = account.AwsSecretAccessKey + } + iamKey := models.AwsIamKey{ Kind: "aws_iam_key", Name: "admin-key", AwsAccessKeyID: account.AwsAccessKeyID, - AwsSecretAccessKey: account.AwsSecretAccessKey, + AwsSecretAccessKey: decrypted, } // For now, an account only has one credential: an IAM key @@ -170,11 +185,11 @@ func makeAccountWithCreds(account AwsAccountDynamoDB) models.AwsAccountWithCreds return result } -// makeAccounts creates new []models.AwsAccount from []AwsAccountDynamoDB -func makeAccounts(accounts []AwsAccountDynamoDB) []models.AwsAccount { - r := []models.AwsAccount{} +// makeAccountsWithCreds creates new []models.AwsAccountWithCreds from []AwsAccountDynamoDB +func (provider *AwsAccountDynamoDBProvider) makeAccountsWithCreds(accounts []AwsAccountDynamoDB) []models.AwsAccountWithCreds { + r := []models.AwsAccountWithCreds{} for _, account := range accounts { - r = append(r, makeAccount(account)) + r = append(r, provider.makeAccountWithCreds(account)) } return r @@ -340,6 +355,16 @@ func (a *AwsAccountDynamoDBProvider) FetchAllByServiceUuid(serviceUuid string) ( return makeAccounts(accounts), nil } +// FetchAllByServiceUuid returns the list of accounts from dynamodb for a specific service uuid +func (a *AwsAccountDynamoDBProvider) FetchAllByServiceUuidWithCreds(serviceUuid string) ([]models.AwsAccountWithCreds, error) { + filter := expression.Name("service_uuid").Equal(expression.Value(serviceUuid)) + accounts, err := GetAccounts(a.Svc, filter, -1) + if err != nil { + return []models.AwsAccountWithCreds{}, err + } + return a.makeAccountsWithCreds(accounts), nil +} + // FetchAllToCleanup returns the list of accounts from dynamodb func (a *AwsAccountDynamoDBProvider) FetchAllToCleanup() ([]models.AwsAccount, error) { filter := expression.Name("to_cleanup").Equal(expression.Value(true)) @@ -482,7 +507,7 @@ func (a *AwsAccountDynamoDBProvider) Request(service_uuid string, count int, ann return []models.AwsAccountWithCreds{}, err } booked.AwsSecretAccessKey = strings.Trim(booked.AwsSecretAccessKey, "\n\r\t ") - bookedAccounts = append(bookedAccounts, makeAccountWithCreds(booked)) + bookedAccounts = append(bookedAccounts, a.makeAccountWithCreds(booked)) count = count - 1 if count == 0 { break @@ -558,5 +583,6 @@ func (a *AwsAccountDynamoDBProvider) DecryptSecret(encrypted string) (string, er if err != nil { return "", err } + str = strings.Trim(string(str), "\r\n\t ") return str, nil } diff --git a/internal/models/aws_account.go b/internal/models/aws_account.go index 7e0afb60..6dd91806 100644 --- a/internal/models/aws_account.go +++ b/internal/models/aws_account.go @@ -60,6 +60,7 @@ type AwsAccountProvider interface { FetchAllToCleanup() ([]AwsAccount, error) FetchAllSorted(by string) ([]AwsAccount, error) FetchAllByServiceUuid(serviceUuid string) ([]AwsAccount, error) + FetchAllByServiceUuidWithCreds(serviceUuid string) ([]AwsAccountWithCreds, error) Request(service_uuid string, count int, annotations map[string]string) ([]AwsAccountWithCreds, error) MarkForCleanup(name string) error MarkForCleanupByServiceUuid(serviceUuid string) error diff --git a/internal/models/placements.go b/internal/models/placements.go index 16ff4e2b..346f4112 100644 --- a/internal/models/placements.go +++ b/internal/models/placements.go @@ -53,6 +53,23 @@ func (p *Placement) LoadResources(accountProvider AwsAccountProvider) error { return nil } +func (p *Placement) LoadResourcesWithCreds(accountProvider AwsAccountProvider) error { + + accounts, err := accountProvider.FetchAllByServiceUuidWithCreds(p.ServiceUuid) + + if err != nil { + return err + } + + p.Resources = []any{} + + for _, account := range accounts { + p.Resources = append(p.Resources, account) + } + + return nil +} + func (p *Placement) Save(dbpool *pgxpool.Pool) error { var id int // Check if placement already exists in the DB diff --git a/todo.org b/todo.org index 322ddd71..88059369 100644 --- a/todo.org +++ b/todo.org @@ -51,7 +51,8 @@ ** DONE create golang channel for stop/start ** DONE parameterize the number of concurrent workers ** TODO create lifecycle handler for placements -* TODO OpenShift limit and req for pods +* DONE OpenShift limit and req for pods +* TODO patch clients (sandbox-list, mark_for_cleanup script, etc) to use the sandbox-API instead of dynamodb * TODO unit tests and fixture/functional tests * TODO documentation coverage * TODO move handlers per version?