From 6a0040f85647c8255a1c1151fa69250e2e427154 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Wed, 10 Sep 2025 13:05:28 +0100 Subject: [PATCH 01/10] Docker image for username service Docker image for the username service used to generate fedid accounts on VMs --- .github/workflows/username_service.yaml | 84 +++++++++++++++++++ stfc-username-service/Dockerfile | 16 ++++ stfc-username-service/go.mod | 7 ++ stfc-username-service/go.sum | 6 ++ stfc-username-service/main.go | 106 ++++++++++++++++++++++++ stfc-username-service/version.txt | 1 + 6 files changed, 220 insertions(+) create mode 100644 .github/workflows/username_service.yaml create mode 100644 stfc-username-service/Dockerfile create mode 100644 stfc-username-service/go.mod create mode 100644 stfc-username-service/go.sum create mode 100644 stfc-username-service/main.go create mode 100644 stfc-username-service/version.txt diff --git a/.github/workflows/username_service.yaml b/.github/workflows/username_service.yaml new file mode 100644 index 00000000..3a4e4481 --- /dev/null +++ b/.github/workflows/username_service.yaml @@ -0,0 +1,84 @@ +name: Username Service + +on: + push: + branches: + - master + pull_request: + paths: + - ".github/workflows/username_service.yaml" + - "stfc-username-service/**" + +jobs: + push_dev_image_harbor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.stfc.ac.uk + username: ${{ secrets.STAGING_HARBOR_USERNAME }} + password: ${{ secrets.STAGING_HARBOR_TOKEN }} + + - name: Set commit SHA for later + id: commit_sha + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Build and push to staging project + uses: docker/build-push-action@v6 + with: + cache-from: type=gha + cache-to: type=gha,mode=max + push: true + context: "{{defaultContext}}:stfc-username-service" + tags: "harbor.stfc.ac.uk/stfc-cloud-staging/stfc-username-service:${{ steps.commit_sha.outputs.sha_short }}" + + - name: Inform of tagged name + run: echo "Image published to harbor.stfc.ac.uk/stfc-cloud-staging/stfc-username-service:${{ steps.commit_sha.outputs.sha_short }}" + + push_release_image_harbor: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.stfc.ac.uk + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_TOKEN }} + + - name: Get release tag for later + id: release_tag + run: echo "version=$(cat stfc-username-service/version.txt)" >> $GITHUB_OUTPUT + + - name: Check if release file has updated + uses: dorny/paths-filter@v3 + id: release_updated + with: + filters: | + version: + - 'stfc-username-service/version.txt' + + - name: Build and push on version change + uses: docker/build-push-action@v6 + if: steps.release_updated.outputs.version == 'true' + with: + cache-from: type=gha + cache-to: type=gha,mode=max + push: true + context: "{{defaultContext}}:stfc-username-service" + tags: "harbor.stfc.ac.uk/stfc-cloud/stfc-username-service:v${{ steps.release_tag.outputs.version }}" + + - name: Inform of tagged name + if: steps.release_updated.outputs.version == 'true' + run: echo "Image published to harbor.stfc.ac.uk/stfc-cloud/stfc-username-service:v${{ steps.release_tag.outputs.version }}" diff --git a/stfc-username-service/Dockerfile b/stfc-username-service/Dockerfile new file mode 100644 index 00000000..e9ea86fc --- /dev/null +++ b/stfc-username-service/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS BuildStage +WORKDIR /app +COPY . . +RUN go mod download && go mod verify +RUN go build -o username . + +FROM alpine:latest AS FinalStage +WORKDIR / +COPY --from=BuildStage /app/username /username + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +RUN apk add --no-cache tini + +USER appuser +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["./username"] \ No newline at end of file diff --git a/stfc-username-service/go.mod b/stfc-username-service/go.mod new file mode 100644 index 00000000..d34ad323 --- /dev/null +++ b/stfc-username-service/go.mod @@ -0,0 +1,7 @@ +module stfc-cloud/username + +go 1.25.1 + +require github.com/gophercloud/gophercloud/v2 v2.8.0 + +require gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/stfc-username-service/go.sum b/stfc-username-service/go.sum new file mode 100644 index 00000000..69739db3 --- /dev/null +++ b/stfc-username-service/go.sum @@ -0,0 +1,6 @@ +github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= +github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/stfc-username-service/main.go b/stfc-username-service/main.go new file mode 100644 index 00000000..82747c8e --- /dev/null +++ b/stfc-username-service/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/v2/openstack/config" + "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" +) + +var identityClient *gophercloud.ServiceClient +var computeClient *gophercloud.ServiceClient + +func init() { + ctx := context.Background() + + var err error + authOptions, endpointOptions, tlsConfig, err := clouds.Parse(clouds.WithCloudName("openstack")) + if err != nil { + panic(err) + } + + authOptions.AllowReauth = true + + providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsConfig)) + if err != nil { + panic(err) + } + + computeClient, err = openstack.NewComputeV2(providerClient, endpointOptions) + if err != nil { + slog.Error("Failed to initilize compute client") + panic(err) + } + identityClient, err = openstack.NewIdentityV3(providerClient, endpointOptions) + if err != nil { + slog.Error("Failed to initilize compute client") + panic(err) + } +} + +func getServerUserID(computeClient *gophercloud.ServiceClient, serverID string) (string, error) { + // Get the user ID associated with a given server + server := servers.Get(context.TODO(), computeClient, serverID) + serverDetails, err := server.Extract() + if err != nil { + slog.Error("Failed to get server details", "ID", serverID) + return "", err + } + + return serverDetails.UserID, nil +} + +func getUsername(identityClient *gophercloud.ServiceClient, userID string) (string, error) { + // Get the username associated with a given user ID + user := users.Get(context.TODO(), identityClient, userID) + userDetails, err := user.Extract() + if err != nil { + slog.Error("Failed to get user details", "ID", userID) + return "", err + } + return userDetails.Name, nil + +} + +func main() { + + addr := os.Getenv("ADDR") + if addr == "" { + addr = ":80" + } + + http.HandleFunc("/getusername", getUserHandler) + err := http.ListenAndServe(addr, nil) + if err != nil { + slog.Error("Failed to start server", "addr", addr) + panic(err) + } +} + +func getUserHandler(w http.ResponseWriter, r *http.Request) { + + serverID := r.URL.Query().Get("serverID") + userID, err := getServerUserID(computeClient, serverID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + name, err := getUsername(identityClient, userID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = fmt.Fprintf(w, "%s", name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/stfc-username-service/version.txt b/stfc-username-service/version.txt new file mode 100644 index 00000000..afaf360d --- /dev/null +++ b/stfc-username-service/version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file From 9e874cddf6c2f3ca634d3b79d38e86cdc3eb97fb Mon Sep 17 00:00:00 2001 From: George Matthews Date: Mon, 15 Sep 2025 11:47:48 +0100 Subject: [PATCH 02/10] Add lint check to ci for username service Add golangci-lint job to ci pipeline for username service docker image --- .github/workflows/username_service.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/username_service.yaml b/.github/workflows/username_service.yaml index 3a4e4481..82e2a2fa 100644 --- a/.github/workflows/username_service.yaml +++ b/.github/workflows/username_service.yaml @@ -10,6 +10,20 @@ on: - "stfc-username-service/**" jobs: + golangci-lint: + name: runner / golangci-lint + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v8.0.0 + with: + working-directory: stfc-username-service/ + push_dev_image_harbor: runs-on: ubuntu-latest steps: From 3fae3bb7ce64dfad1950142f314d9f2566d4bde3 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Mon, 15 Dec 2025 18:42:59 +0000 Subject: [PATCH 03/10] Refactor username service to use structs Stop using init() not recommended method to set up connections also made it difficult to add tests. Use client struct instead. Add user handler to a service struct so the function can use the client --- stfc-username-service/client.go | 44 +++++++++++++++++++++++++++++ stfc-username-service/main.go | 49 ++++++++------------------------- 2 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 stfc-username-service/client.go diff --git a/stfc-username-service/client.go b/stfc-username-service/client.go new file mode 100644 index 00000000..830c9830 --- /dev/null +++ b/stfc-username-service/client.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "log/slog" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/config" + "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" +) + +type OpenstackClient struct { + computeClient *gophercloud.ServiceClient + identityClient *gophercloud.ServiceClient +} + +func (c *OpenstackClient) New(cloud string) { + ctx := context.Background() + + authOptions, endpointOptions, tlsConfig, err := clouds.Parse(clouds.WithCloudName(cloud)) + if err != nil { + panic(err) + } + + authOptions.AllowReauth = true + + providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsConfig)) + if err != nil { + panic(err) + } + + c.computeClient, err = openstack.NewComputeV2(providerClient, endpointOptions) + if err != nil { + slog.Error("Failed to initilize compute client") + panic(err) + } + c.identityClient, err = openstack.NewIdentityV3(providerClient, endpointOptions) + if err != nil { + slog.Error("Failed to initilize identity client") + panic(err) + } + +} diff --git a/stfc-username-service/main.go b/stfc-username-service/main.go index 82747c8e..582edf04 100644 --- a/stfc-username-service/main.go +++ b/stfc-username-service/main.go @@ -8,42 +8,12 @@ import ( "os" "github.com/gophercloud/gophercloud/v2" - "github.com/gophercloud/gophercloud/v2/openstack" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" - "github.com/gophercloud/gophercloud/v2/openstack/config" - "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" ) -var identityClient *gophercloud.ServiceClient -var computeClient *gophercloud.ServiceClient - -func init() { - ctx := context.Background() - - var err error - authOptions, endpointOptions, tlsConfig, err := clouds.Parse(clouds.WithCloudName("openstack")) - if err != nil { - panic(err) - } - - authOptions.AllowReauth = true - - providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsConfig)) - if err != nil { - panic(err) - } - - computeClient, err = openstack.NewComputeV2(providerClient, endpointOptions) - if err != nil { - slog.Error("Failed to initilize compute client") - panic(err) - } - identityClient, err = openstack.NewIdentityV3(providerClient, endpointOptions) - if err != nil { - slog.Error("Failed to initilize compute client") - panic(err) - } +type service struct { + client OpenstackClient } func getServerUserID(computeClient *gophercloud.ServiceClient, serverID string) (string, error) { @@ -66,8 +36,8 @@ func getUsername(identityClient *gophercloud.ServiceClient, userID string) (stri slog.Error("Failed to get user details", "ID", userID) return "", err } - return userDetails.Name, nil + return userDetails.Name, nil } func main() { @@ -77,7 +47,12 @@ func main() { addr = ":80" } - http.HandleFunc("/getusername", getUserHandler) + var client OpenstackClient + client.New("openstack") + + service := service{client: client} + + http.HandleFunc("/getusername", service.getUserHandler) err := http.ListenAndServe(addr, nil) if err != nil { slog.Error("Failed to start server", "addr", addr) @@ -85,15 +60,15 @@ func main() { } } -func getUserHandler(w http.ResponseWriter, r *http.Request) { +func (s service) getUserHandler(w http.ResponseWriter, r *http.Request) { serverID := r.URL.Query().Get("serverID") - userID, err := getServerUserID(computeClient, serverID) + userID, err := getServerUserID(s.client.identityClient, serverID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - name, err := getUsername(identityClient, userID) + name, err := getUsername(s.client.identityClient, userID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return From c3982b11b3ba6bd0549e7a96a7630d7b4bcf1f97 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Tue, 16 Dec 2025 09:52:22 +0000 Subject: [PATCH 04/10] Add tests for getting username details from openstack Add success and failure tests for getting user details from openstack for the username service --- stfc-username-service/main_test.go | 148 +++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 stfc-username-service/main_test.go diff --git a/stfc-username-service/main_test.go b/stfc-username-service/main_test.go new file mode 100644 index 00000000..07a7dc44 --- /dev/null +++ b/stfc-username-service/main_test.go @@ -0,0 +1,148 @@ +package main + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/v2/testhelper" + "github.com/gophercloud/gophercloud/v2/testhelper/client" +) + +type response func() + +func TestUserNameFailure(t *testing.T) { + tests := []struct { + desc string + arg string + fake response + expOutput string + expErr string + }{{ + desc: "user find error", + arg: "abc123", + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fakeserver := testhelper.SetupHTTP() + defer fakeserver.Teardown() + url := "/users/" + tt.arg + fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + c := client.ServiceClient(fakeserver) + + r, err := getUsername(c, tt.arg) + testhelper.AssertErr(t, err) + testhelper.AssertEquals(t, r, "") + }) + } +} + +func TestUserNameSuccess(t *testing.T) { + tests := []struct { + desc string + arg string + fake response + expOutput string + expErr string + }{{ + desc: "user find success", + arg: "abc123", + expOutput: "test_user", + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fakeserver := testhelper.SetupHTTP() + defer fakeserver.Teardown() + url := "/users/" + tt.arg + fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "user": + { + "id": "abc123", + "name": "test_user" + } + } + `) + }) + c := client.ServiceClient(fakeserver) + + r, err := getUsername(c, tt.arg) + testhelper.AssertNoErr(t, err) + testhelper.AssertEquals(t, r, "test_user") + }) + } +} + +func TestServerUserIDFailure(t *testing.T) { + tests := []struct { + desc string + arg string + fake response + expOutput string + expErr string + }{{ + desc: "server user ID find error", + arg: "xyz321", + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fakeserver := testhelper.SetupHTTP() + defer fakeserver.Teardown() + url := "/servers/" + tt.arg + fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + c := client.ServiceClient(fakeserver) + + r, err := getServerUserID(c, tt.arg) + testhelper.AssertErr(t, err) + testhelper.AssertEquals(t, r, "") + }) + } +} + +func TestServerUserIDSuccess(t *testing.T) { + tests := []struct { + desc string + arg string + fake response + expOutput string + expErr string + }{{ + desc: "server user ID find success", + arg: "xyz321", + expOutput: "abc123", + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fakeserver := testhelper.SetupHTTP() + defer fakeserver.Teardown() + url := "/servers/" + tt.arg + fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "server": + { + "id": "xyz321", + "user_id": "abc123" + } + } + `) + }) + c := client.ServiceClient(fakeserver) + + r, err := getServerUserID(c, tt.arg) + testhelper.AssertNoErr(t, err) + testhelper.AssertEquals(t, r, "abc123") + }) + } +} From 04217cf4a29e674ec6fda5c7e5678297440753e5 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Tue, 16 Dec 2025 09:53:42 +0000 Subject: [PATCH 05/10] Add run-test stage to dockerfile for userame service Add a stage to run go tests in the docker image build process. This was following steps from docker docs here: https://docs.docker.com/guides/golang/run-tests/ --- stfc-username-service/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stfc-username-service/Dockerfile b/stfc-username-service/Dockerfile index e9ea86fc..cd352c8d 100644 --- a/stfc-username-service/Dockerfile +++ b/stfc-username-service/Dockerfile @@ -4,6 +4,10 @@ COPY . . RUN go mod download && go mod verify RUN go build -o username . +# Run the tests in the container +FROM BuildStage AS TestStage +RUN go test -v ./... + FROM alpine:latest AS FinalStage WORKDIR / COPY --from=BuildStage /app/username /username From 74f529fbe443df0a62b05239c2d9897b12b71ca9 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Tue, 16 Dec 2025 09:57:25 +0000 Subject: [PATCH 06/10] Rename dockerfile stage names Rename stage names to conform to linting requirments --- stfc-username-service/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stfc-username-service/Dockerfile b/stfc-username-service/Dockerfile index cd352c8d..2e175301 100644 --- a/stfc-username-service/Dockerfile +++ b/stfc-username-service/Dockerfile @@ -1,16 +1,16 @@ -FROM golang:1.25-alpine AS BuildStage +FROM golang:1.25-alpine AS build-stage WORKDIR /app COPY . . RUN go mod download && go mod verify RUN go build -o username . # Run the tests in the container -FROM BuildStage AS TestStage +FROM build-stage AS test-stage RUN go test -v ./... -FROM alpine:latest AS FinalStage +FROM alpine:latest AS build-release-stage WORKDIR / -COPY --from=BuildStage /app/username /username +COPY --from=build-stage /app/username /username RUN addgroup -S appgroup && adduser -S appuser -G appgroup RUN apk add --no-cache tini From 7c75e4ab5d7afbe2e568bb3326930e58a736a332 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Tue, 16 Dec 2025 10:08:41 +0000 Subject: [PATCH 07/10] Add job to github workflow to run go tests in container Add additional job to the github workflow for the username service which runs the tests inside the conainer, make build and push stages depend on the test and lint jobs --- .github/workflows/username_service.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/username_service.yaml b/.github/workflows/username_service.yaml index 82e2a2fa..b28f6e8d 100644 --- a/.github/workflows/username_service.yaml +++ b/.github/workflows/username_service.yaml @@ -24,8 +24,26 @@ jobs: with: working-directory: stfc-username-service/ + run_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run tests in container + uses: docker/build-push-action@v6 + with: + no-cache: true + cache-to: type=gha,mode=max + target: test-stage + push: false + context: "{{defaultContext}}:stfc-username-service" + push_dev_image_harbor: runs-on: ubuntu-latest + needs: [run_tests, golangci-lint] steps: - uses: actions/checkout@v5 @@ -48,7 +66,7 @@ jobs: with: cache-from: type=gha cache-to: type=gha,mode=max - push: true + push: false context: "{{defaultContext}}:stfc-username-service" tags: "harbor.stfc.ac.uk/stfc-cloud-staging/stfc-username-service:${{ steps.commit_sha.outputs.sha_short }}" @@ -57,6 +75,7 @@ jobs: push_release_image_harbor: runs-on: ubuntu-latest + needs: [run_tests, golangci-lint] if: github.ref == 'refs/heads/master' steps: - uses: actions/checkout@v5 From f34937d9a86b248b31f7ff2bc53caaaae6bb60b7 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Tue, 16 Dec 2025 10:34:47 +0000 Subject: [PATCH 08/10] Fix go lint errors in tests Add error checks in the go tests --- stfc-username-service/main_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/stfc-username-service/main_test.go b/stfc-username-service/main_test.go index 07a7dc44..75226f7e 100644 --- a/stfc-username-service/main_test.go +++ b/stfc-username-service/main_test.go @@ -60,7 +60,7 @@ func TestUserNameSuccess(t *testing.T) { url := "/users/" + tt.arg fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") - fmt.Fprintf(w, ` + _, err := fmt.Fprintf(w, ` { "user": { @@ -69,6 +69,10 @@ func TestUserNameSuccess(t *testing.T) { } } `) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } }) c := client.ServiceClient(fakeserver) @@ -128,7 +132,7 @@ func TestServerUserIDSuccess(t *testing.T) { url := "/servers/" + tt.arg fakeserver.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") - fmt.Fprintf(w, ` + _, err := fmt.Fprintf(w, ` { "server": { @@ -137,6 +141,10 @@ func TestServerUserIDSuccess(t *testing.T) { } } `) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } }) c := client.ServiceClient(fakeserver) From 5cf08ebb4fb61cd57491d6b9a3fcb3be3d471faf Mon Sep 17 00:00:00 2001 From: George Matthews Date: Mon, 26 Jan 2026 09:39:11 +0000 Subject: [PATCH 09/10] Restructure main.go Move func main to be last function --- stfc-username-service/main.go | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/stfc-username-service/main.go b/stfc-username-service/main.go index 582edf04..14ce8c87 100644 --- a/stfc-username-service/main.go +++ b/stfc-username-service/main.go @@ -40,26 +40,6 @@ func getUsername(identityClient *gophercloud.ServiceClient, userID string) (stri return userDetails.Name, nil } -func main() { - - addr := os.Getenv("ADDR") - if addr == "" { - addr = ":80" - } - - var client OpenstackClient - client.New("openstack") - - service := service{client: client} - - http.HandleFunc("/getusername", service.getUserHandler) - err := http.ListenAndServe(addr, nil) - if err != nil { - slog.Error("Failed to start server", "addr", addr) - panic(err) - } -} - func (s service) getUserHandler(w http.ResponseWriter, r *http.Request) { serverID := r.URL.Query().Get("serverID") @@ -79,3 +59,23 @@ func (s service) getUserHandler(w http.ResponseWriter, r *http.Request) { return } } + +func main() { + + addr := os.Getenv("ADDR") + if addr == "" { + addr = ":80" + } + + var client OpenstackClient + client.New("openstack") + + service := service{client: client} + + http.HandleFunc("/getusername", service.getUserHandler) + err := http.ListenAndServe(addr, nil) + if err != nil { + slog.Error("Failed to start server", "addr", addr) + panic(err) + } +} From 833ec9e249ae481988205472b065b3541cc20a98 Mon Sep 17 00:00:00 2001 From: George Matthews Date: Mon, 26 Jan 2026 09:40:01 +0000 Subject: [PATCH 10/10] Add comments to funcs Add comments for functions in username service --- stfc-username-service/client.go | 3 +++ stfc-username-service/main.go | 5 +++-- stfc-username-service/main_test.go | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/stfc-username-service/client.go b/stfc-username-service/client.go index 830c9830..6389d2ae 100644 --- a/stfc-username-service/client.go +++ b/stfc-username-service/client.go @@ -10,11 +10,14 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" ) +// Struct containing openstack service clients for interacting with the API type OpenstackClient struct { computeClient *gophercloud.ServiceClient identityClient *gophercloud.ServiceClient } +// Creates a new client which can be used to interact with the API +// uses a cloud defined in cloud.yaml func (c *OpenstackClient) New(cloud string) { ctx := context.Background() diff --git a/stfc-username-service/main.go b/stfc-username-service/main.go index 14ce8c87..dc46b153 100644 --- a/stfc-username-service/main.go +++ b/stfc-username-service/main.go @@ -16,8 +16,8 @@ type service struct { client OpenstackClient } +// Get the user ID associated with a given server func getServerUserID(computeClient *gophercloud.ServiceClient, serverID string) (string, error) { - // Get the user ID associated with a given server server := servers.Get(context.TODO(), computeClient, serverID) serverDetails, err := server.Extract() if err != nil { @@ -28,8 +28,8 @@ func getServerUserID(computeClient *gophercloud.ServiceClient, serverID string) return serverDetails.UserID, nil } +// Get the username associated with a given user ID func getUsername(identityClient *gophercloud.ServiceClient, userID string) (string, error) { - // Get the username associated with a given user ID user := users.Get(context.TODO(), identityClient, userID) userDetails, err := user.Extract() if err != nil { @@ -40,6 +40,7 @@ func getUsername(identityClient *gophercloud.ServiceClient, userID string) (stri return userDetails.Name, nil } +// Handle requests to the path: .../getusername?serverID= func (s service) getUserHandler(w http.ResponseWriter, r *http.Request) { serverID := r.URL.Query().Get("serverID") diff --git a/stfc-username-service/main_test.go b/stfc-username-service/main_test.go index 75226f7e..068508c5 100644 --- a/stfc-username-service/main_test.go +++ b/stfc-username-service/main_test.go @@ -11,6 +11,7 @@ import ( type response func() +// Test error on returned status code 404 func TestUserNameFailure(t *testing.T) { tests := []struct { desc string @@ -40,6 +41,7 @@ func TestUserNameFailure(t *testing.T) { } } +// Test username is returned when user is present func TestUserNameSuccess(t *testing.T) { tests := []struct { desc string @@ -83,6 +85,7 @@ func TestUserNameSuccess(t *testing.T) { } } +// Test error when server 404 status code returned func TestServerUserIDFailure(t *testing.T) { tests := []struct { desc string @@ -112,6 +115,7 @@ func TestServerUserIDFailure(t *testing.T) { } } +// Test server ID is returned when a server is found func TestServerUserIDSuccess(t *testing.T) { tests := []struct { desc string