diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..b23c435b9 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,25 @@ +name: Run golangci-lint +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull requests. Use with `only-new-issues` option. + # pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 \ No newline at end of file diff --git a/.goreleaser-darwin.yaml b/.goreleaser-darwin.yaml new file mode 100644 index 000000000..0a0eb975d --- /dev/null +++ b/.goreleaser-darwin.yaml @@ -0,0 +1,42 @@ +version: 2 +project_name: cloud-init +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + - go install github.com/swaggo/swag/cmd/swag@latest + - swag init -g cmd/cloud-init-server/main.go + +builds: + - id: darwin + main: ./cmd/cloud-init-server + binary: cloud-init-server + # export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) + # export BUILD_HOST=$(hostname) + # export GO_VERSION=$(go version | awk '{print $3}') + # export BUILD_USER=$(whoami) + ldflags: + - "-X 'main.GitCommit={{.Commit}}' \ + -X 'main.BuildTime={{.Timestamp}}' \ + -X 'main.Version={{.Version}}' \ + -X 'main.GitBranch={{.Branch}}' \ + -X 'main.GitTag={{.Tag}}' \ + -X 'main.GitState={{ .Env.GIT_STATE }}' \ + -X 'main.BuildHost={{ .Env.BUILD_HOST }}' \ + -X 'main.GoVersion={{ .Env.GO_VERSION }}' \ + -X 'main.BuildUser={{ .Env.BUILD_USER }}'" + goos: + - darwin + goarch: + - amd64 + - arm64 + goamd64: + - v3 + - v1 + env: + - CGO_ENABLED=1 + +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://raw.githubusercontent.com/goreleaser/goreleaser/v2.11.2/www/docs/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7add4f407..d6398823c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,14 +1,14 @@ - -version: 2.4 +version: 2 project_name: cloud-init before: hooks: # You may remove this if you don't use go modules. - go mod tidy + - go install github.com/swaggo/swag/cmd/swag@latest - swag init -g cmd/cloud-init-server/main.go builds: - - id: cloud-init + - id: linux main: ./cmd/cloud-init-server binary: cloud-init-server # export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) @@ -27,14 +27,17 @@ builds: -X 'main.BuildUser={{ .Env.BUILD_USER }}'" goos: - linux - - darwin goarch: - amd64 - arm64 goamd64: - v3 + - v1 env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 + - CC={{ if eq .Arch "arm64" }}aarch64-linux-gnu-gcc{{ else }}gcc{{ end }} + - CXX={{ if eq .Arch "arm64" }}aarch64-linux-gnu-g++{{ else }}g++{{ end }} + dockers: - image_templates: @@ -125,5 +128,5 @@ changelog: # The lines beneath this are called `modelines`. See `:help modeline` # Feel free to remove those if you don't want/use them. -# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/goreleaser/goreleaser/v2.11.2/www/docs/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/Dockerfile b/Dockerfile index dee27b0ce..d961c8ca7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,65 @@ +# syntax=docker/dockerfile:1.7 +FROM ubuntu:24.04 AS duckdb-ext +ARG TARGETARCH +ARG DUCKDB_VERSION="1.4.0" +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl unzip \ + && rm -rf /var/lib/apt/lists/* -FROM chainguard/wolfi-base:latest +# Map TARGETARCH (docker buildx) or uname -m to DuckDB CLI artifact name +# amd64|x86_64 => linux-amd64, arm64|aarch64 => linux-arm64 (v1.4.0 release assets) +RUN ARCH="${TARGETARCH:-$(uname -m)}" \ + && case "${ARCH}" in \ + amd64|x86_64) CLI_URL="https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb_cli-linux-amd64.zip" ;; \ + arm64|aarch64) CLI_URL="https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb_cli-linux-arm64.zip" ;; \ + *) echo "unsupported arch: ${ARCH}" && exit 1 ;; \ + esac \ + && curl -fsSL "$CLI_URL" -o /tmp/duckdb.zip \ + && unzip /tmp/duckdb.zip -d /usr/local/bin \ + && chmod +x /usr/local/bin/duckdb \ + && rm -f /tmp/duckdb.zip -RUN apk add --no-cache tini +ENV DUCKDB_HOME=/duckdb_home +RUN mkdir -p "$DUCKDB_HOME" -# Include curl in the final image. -RUN set -ex \ - && apk update \ - && apk add --no-cache curl tini wireguard-tools iputils iproute2 \ - && rm -rf /var/cache/apk/* \ - && rm -rf /tmp/* -STOPSIGNAL SIGTERM +# Preinstall whatever you need +RUN duckdb -c "INSTALL 'json';" \ + && duckdb -c "INSTALL 'parquet';" -# Get the boot-script-service from the builder stage. -COPY cloud-init-server /usr/local/bin/ +# -------- runtime image -------- +FROM ubuntu:24.04 +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates wireguard-tools tini \ + && rm -rf /var/lib/apt/lists/* + +COPY cloud-init-server /usr/local/bin/cloud-init-server + +#Set the golang modcache +ENV GOMODCACHE=/tmp/.cache +RUN mkdir -p "$GOMODCACHE" && chown 65534:65534 "$GOMODCACHE" +ENV XDG_CACHE_HOME=/tmp/.cache + + +ENV DUCKDB_HOME=/opt/duckdbhome +RUN mkdir -p "$DUCKDB_HOME" && chown 65534:65534 "$DUCKDB_HOME" + +# Copy the preinstalled extensions (built for the current arch) +COPY --from=duckdb-ext /duckdb_home/ ${DUCKDB_HOME}/ +RUN chown -R 65534:65534 ${DUCKDB_HOME} + +# Store data in $DATADIR +ENV DATADIR=/data +RUN mkdir -p "${DATADIR}" +VOLUME "$DATADIR" +RUN chown -R 65534:65534 ${DATADIR} ENV TOKEN_URL="http://opaal:3333/token" ENV SMD_URL="http://smd:27779" ENV LISTEN="0.0.0.0:27777" - -# nobody 65534:65534 USER 65534:65534 +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/local/bin/cloud-init-server"] -# Set up the command to start the service. -CMD /usr/local/bin/cloud-init-server - -ENTRYPOINT ["/sbin/tini", "--"] diff --git a/build-in-container.sh b/build-in-container.sh new file mode 100755 index 000000000..73c3586fb --- /dev/null +++ b/build-in-container.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# This script uses the latest Ubuntu 24.04 container to build the project with GoReleaser. It emulates the GitHub Actions environment as closely as possible. +# Before submitting a PR for release/build. please run this script to ensure your PR will pass the build. + +# Name of the container +CONTAINER_NAME="goreleaser-build" + +# Directory where built binaries will be available +OUTPUT_DIR="$(pwd)/dist" + +export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) +export BUILD_HOST=$(hostname) +export GO_VERSION=$(go version | awk '{print $3}') +export BUILD_USER=$(whoami) + +# Start a new disposable Ubuntu 24.04 container with the current directory mounted +${CONTAINER_CMD:-docker} run --rm -it \ + --name "$CONTAINER_NAME" \ + -v "$(pwd)":/workspace \ + -v ${CONTAINER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock \ + -w /workspace \ + ubuntu:24.04 bash -c " + + # Suppress timezone prompts + export DEBIAN_FRONTEND=noninteractive + export TZ=UTC + + # Update package lists and install dependencies + apt update && apt install -y git gcc g++ make \ + ca-certificates curl gnupg \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libc6-dev-arm64-cross software-properties-common \ + clang-tools libstdc++-13-dev-arm64-cross + + install -m 0755 -d /etc/apt/keyrings + + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ + gpg --dearmor -o /etc/apt/keyrings/docker.gpg + + echo \ + \"deb [arch=\$(dpkg --print-architecture) \ + signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable\" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null + + apt update && apt install -y \ + docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Install Go (match GitHub runner version) + curl -fsSL https://golang.org/dl/go1.21.5.linux-amd64.tar.gz | tar -C /usr/local -xz + export PATH=\$PATH:/usr/local/go/bin + go version # Verify Go installation + + # Set GOPATH and update PATH to include Go binaries + export GOPATH=\$(go env GOPATH) + export PATH=\$PATH:\$GOPATH/bin + echo \"GOPATH: \$GOPATH\" && echo \"PATH: \$PATH\" + + # Install Goreleaser + curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin + goreleaser --version # Verify Goreleaser installation + + # Setup Docker buildx for multi-platform builds + docker buildx create --use --name goreleaser-builder || true + docker buildx inspect --bootstrap + + # Set Build Environment Variables + export GIT_STATE="$GIT_STATE" + export BUILD_HOST="$BUILD_HOST" + export BUILD_USER="$BUILD_USER" + export GO_VERSION=$(go version | awk '{print $3}') + + # Convince git that our directory is safe + git config --global --add safe.directory /workspace + + # Run Goreleaser + goreleaser release --snapshot --clean --skip archive,publish +" + +# Notify user of success +echo "✅ Build complete! Check the output in: $OUTPUT_DIR" diff --git a/cmd/cloud-init-server/group_handlers.go b/cmd/cloud-init-server/group_handlers.go index 029be13da..ca0e265b2 100644 --- a/cmd/cloud-init-server/group_handlers.go +++ b/cmd/cloud-init-server/group_handlers.go @@ -18,7 +18,7 @@ import ( // @Produce json // @Success 200 {object} map[string]cistore.ClusterDefaults // @Failure 500 {object} nil -// @Router /cloud-init/admin/groups [get] +// @Router /admin/groups [get] func (h CiHandler) GetGroups(w http.ResponseWriter, r *http.Request) { var ( groups map[string]cistore.GroupData @@ -58,7 +58,7 @@ func (h CiHandler) GetGroups(w http.ResponseWriter, r *http.Request) { // @Failure 422 {object} nil // @Header 201 {string} Location "/groups/{id}" // @Param group body cistore.GroupData true "Group data" -// @Router /cloud-init/admin/groups [post] +// @Router /admin/groups [post] func (h CiHandler) AddGroupHandler(w http.ResponseWriter, r *http.Request) { var ( data cistore.GroupData @@ -91,14 +91,15 @@ func (h CiHandler) AddGroupHandler(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} cistore.GroupData // @Failure 500 {object} nil // @Param id path string true "Group ID" -// @Router /cloud-init/admin/groups/{id} [get] +// @Router /admin/groups/{id} [get] func (h CiHandler) GetGroupHandler(w http.ResponseWriter, r *http.Request) { var ( - id string = chi.URLParam(r, "id") + id string data cistore.GroupData bytes []byte err error ) + id = chi.URLParam(r, "id") data, err = h.store.GetGroupData(id) if err != nil { @@ -111,7 +112,10 @@ func (h CiHandler) GetGroupHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Write(bytes) + if _, err := w.Write(bytes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } // UpdateGroupHandler godoc @@ -132,14 +136,16 @@ func (h CiHandler) GetGroupHandler(w http.ResponseWriter, r *http.Request) { // @Header 201 {string} Location "/groups/{name}" // @Param name path string true "Group name" // @Param group_data body cistore.GroupData true "Group data" -// @Router /cloud-init/admin/groups/{name} [put] +// @Router /admin/groups/{name} [put] func (h CiHandler) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) { var ( - groupName string = chi.URLParam(r, "name") + groupName string data cistore.GroupData err error ) + groupName = chi.URLParam(r, "name") + data, err = parseData(r) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) @@ -164,12 +170,13 @@ func (h CiHandler) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} nil // @Failure 500 {object} nil // @Param id path string true "Group ID" -// @Router /cloud-init/admin/groups/{id} [delete] +// @Router /admin/groups/{id} [delete] func (h CiHandler) RemoveGroupHandler(w http.ResponseWriter, r *http.Request) { var ( - id string = chi.URLParam(r, "id") + id string err error ) + id = chi.URLParam(r, "id") err = h.store.RemoveGroupData(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/cloud-init-server/handlers.go b/cmd/cloud-init-server/handlers.go index 1e2aa5d00..6ed501421 100644 --- a/cmd/cloud-init-server/handlers.go +++ b/cmd/cloud-init-server/handlers.go @@ -53,7 +53,7 @@ func parseData(r *http.Request) (cistore.GroupData, error) { // @Produce json // @Success 200 {object} string // @Failure 500 {object} nil -// @Router /cloud-init/openapi.json [get] +// @Router /openapi.json [get] func DocsHandler(w http.ResponseWriter, r *http.Request) { doc, err := swag.ReadDoc() if err != nil { @@ -62,7 +62,11 @@ func DocsHandler(w http.ResponseWriter, r *http.Request) { return } bDoc := []byte(doc) - w.Write(bDoc) + if _, err := w.Write(bDoc); err != nil { + log.Error().Msgf("Error writing OpenAPI docs: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } } // SetClusterDataHandler godoc @@ -75,7 +79,7 @@ func DocsHandler(w http.ResponseWriter, r *http.Request) { // @Failure 400 {object} nil // @Failure 500 {object} nil // @Param data body cistore.ClusterDefaults true "Cluster defaults data" -// @Router /cloud-init/admin/cluster-defaults [post] +// @Router /admin/cluster-defaults [post] func SetClusterDataHandler(store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -113,7 +117,7 @@ func SetClusterDataHandler(store cistore.Store) http.HandlerFunc { // @Produce json // @Success 200 {object} cistore.ClusterDefaults // @Failure 500 {object} nil -// @Router /cloud-init/admin/cluster-defaults [get] +// @Router /admin/cluster-defaults [get] func GetClusterDataHandler(store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data, err := store.GetClusterDefaults() @@ -132,7 +136,9 @@ func GetClusterDataHandler(store cistore.Store) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(jsonData) + if _, err = w.Write(jsonData); err != nil { + log.Error().Err(err).Msg("failed to write response") + } } } @@ -147,7 +153,7 @@ func GetClusterDataHandler(store cistore.Store) http.HandlerFunc { // @Failure 500 {object} nil // @Param id path string true "Node ID" // @Param instance-info body cistore.OpenCHAMIInstanceInfo true "Instance info data" -// @Router /cloud-init/admin/instance-info/{id} [put] +// @Router /admin/instance-info/{id} [put] func InstanceInfoHandler(sm smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { @@ -155,7 +161,7 @@ func InstanceInfoHandler(sm smdclient.SMDClientInterface, store cistore.Store) h return } - var id string = chi.URLParam(r, "id") + id := chi.URLParam(r, "id") var info cistore.OpenCHAMIInstanceInfo body, err := io.ReadAll(r.Body) if err != nil { @@ -200,7 +206,7 @@ func InstanceInfoHandler(sm smdclient.SMDClientInterface, store cistore.Store) h // @Param instance_id formData string true "Node's given instance ID" // @Param hostname formData string true "Node's given hostname" // @Param fqdn formData string true "Node's given fully-qualified domain name" -// @Router /cloud-init/phone-home/{id} [post] +// @Router /phone-home/{id} [post] func PhoneHomeHandler(wg *wgtunnel.InterfaceManager, sm smdclient.SMDClientInterface) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -245,7 +251,7 @@ func PhoneHomeHandler(wg *wgtunnel.InterfaceManager, sm smdclient.SMDClientInter if wg != nil { go func() { - wg.RemovePeer(peerName) + _ = wg.RemovePeer(peerName) // Explicitly ignoring the error here. There's nothing to do with it within the goroutine. }() w.WriteHeader(http.StatusOK) diff --git a/cmd/cloud-init-server/main.go b/cmd/cloud-init-server/main.go index c6a194746..e54e71812 100644 --- a/cmd/cloud-init-server/main.go +++ b/cmd/cloud-init-server/main.go @@ -8,7 +8,6 @@ package main import ( "fmt" - "net" "net/http" "os" "strings" @@ -16,6 +15,7 @@ import ( "github.com/OpenCHAMI/cloud-init/internal/memstore" openchami_middleware "github.com/OpenCHAMI/cloud-init/internal/middleware" + "github.com/OpenCHAMI/cloud-init/internal/quackstore" "github.com/OpenCHAMI/cloud-init/internal/smdclient" "github.com/OpenCHAMI/cloud-init/pkg/cistore" "github.com/OpenCHAMI/cloud-init/pkg/wgtunnel" @@ -52,6 +52,8 @@ var ( wireguardOnly bool debug bool wireGuardMiddleware func(http.Handler) http.Handler + storageBackend = "mem" // Default to memstore + dbPath = "cloud-init.db" // Default database path for quackstore store cistore.Store ) @@ -93,27 +95,33 @@ func setupFlags(flags *pflag.FlagSet) { flags.StringVar(&wireguardServer, "wireguard-server", getEnv("WIREGUARD_SERVER", ""), "WireGuard server IP address and network (e.g. 100.97.0.1/16)") flags.BoolVar(&wireguardOnly, "wireguard-only", parseBool(getEnv("WIREGUARD_ONLY", "false")), "Only allow access to the cloud-init functions from the WireGuard subnet") flags.BoolVar(&debug, "debug", parseBool(getEnv("DEBUG", "false")), "Enable debug logging") + flags.StringVar(&storageBackend, "storage-backend", getEnv("STORAGE_BACKEND", "mem"), "Storage backend to use (mem or quack)") + flags.StringVar(&dbPath, "db-path", getEnv("DB_PATH", "cloud-init.db"), "Path to the database file for quackstore backend") } // bindViperToFlags binds each flag to Viper so environment variables work seamlessly. func bindViperToFlags() { viper.AutomaticEnv() - viper.BindEnv("listen") - viper.BindEnv("token_url") - viper.BindEnv("smd_url") - viper.BindEnv("jwks_url") - viper.BindEnv("access_token") - viper.BindEnv("cluster_name") - viper.BindEnv("region") - viper.BindEnv("az") - viper.BindEnv("cloud_provider") - viper.BindEnv("base_url") - viper.BindEnv("cacert") - viper.BindEnv("insecure") - viper.BindEnv("impersonation") - viper.BindEnv("wireguard_server") - viper.BindEnv("wireguard_only") - viper.BindEnv("debug") + // Bind each flag to its corresponding environment variable + // Not checking errors because these always succeed + _ = viper.BindEnv("listen") + _ = viper.BindEnv("token_url") + _ = viper.BindEnv("smd_url") + _ = viper.BindEnv("jwks_url") + _ = viper.BindEnv("access_token") + _ = viper.BindEnv("cluster_name") + _ = viper.BindEnv("region") + _ = viper.BindEnv("az") + _ = viper.BindEnv("cloud_provider") + _ = viper.BindEnv("base_url") + _ = viper.BindEnv("cacert") + _ = viper.BindEnv("insecure") + _ = viper.BindEnv("impersonation") + _ = viper.BindEnv("wireguard_server") + _ = viper.BindEnv("wireguard_only") + _ = viper.BindEnv("debug") + _ = viper.BindEnv("storage_backend") + _ = viper.BindEnv("db_path") } // startServer is where we run our main program logic @@ -137,6 +145,8 @@ func startServer() error { Str("wireguard-server", wireguardServer). Bool("wireguard-only", wireguardOnly). Bool("debug", debug). + Str("storage-backend", storageBackend). + Str("db-path", dbPath). Msg("Resolved configuration") } @@ -145,6 +155,20 @@ func startServer() error { zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + // Initialize storage backend + var err error + switch storageBackend { + case "mem": + store = memstore.NewMemStore() + case "quack": + store, err = quackstore.NewQuackStore(dbPath) + if err != nil { + return fmt.Errorf("failed to initialize quackstore: %w", err) + } + default: + return fmt.Errorf("unsupported storage backend: %s", storageBackend) + } + // Setup JWKS if provided var keyset *jwtauth.JWTAuth secureRouteEnable := false @@ -164,61 +188,29 @@ func startServer() error { var sm smdclient.SMDClientInterface if os.Getenv("CLOUD_INIT_SMD_SIMULATOR") == "true" { fmt.Printf("\n\n**********\n\n\tCLOUD_INIT_SMD_SIMULATOR is set to true in your environment.\n\n\tUsing the FakeSMDClient\n\n**********\n\n\n") - fakeSMDEnabled = true - fakeSm := smdclient.NewFakeSMDClient(clusterName, 500) - fakeSm.Summary() - sm = fakeSm + sm = smdclient.NewFakeSMDClient(clusterName, 500) } else { - var err error sm, err = smdclient.NewSMDClient(clusterName, smdEndpoint, tokenEndpoint, accessToken, certPath, insecure) if err != nil { - log.Fatal().Err(err) + return fmt.Errorf("failed to create SMD client: %w", err) } } - // Initialize in-memory store - store = memstore.NewMemStore() - store.SetClusterDefaults(cistore.ClusterDefaults{ - ClusterName: clusterName, - Region: region, - AvailabilityZone: availabilityZone, - CloudProvider: cloudProvider, - BaseUrl: baseUrl, - }) - - // Initialize the cloud-init handler - ciHandler := NewCiHandler(store, sm, clusterName) - - // WireGuard - var wgInterfaceManager *wgtunnel.InterfaceManager - if wireguardServer != "" { - log.Info().Msgf("Initializing WireGuard server with %s", wireguardServer) - wgIp, wgNet, err := net.ParseCIDR(wireguardServer) - if err != nil { - log.Fatal().Err(err).Msgf("Failed to parse WireGuard server IP and netmask from %s. Use format '100.97.0.1/16'", wireguardServer) - } - wgInterfaceManager = wgtunnel.NewInterfaceManager("wg0", wgIp, wgNet) - err = wgInterfaceManager.StartServer() - if err != nil { - log.Fatal().Err(err).Msg("Failed to start the WireGuard server") - } + // Create CI handler + handler := &CiHandler{ + sm: sm, + store: store, } - // If wireguard-only is set, only allow access within the WireGuard subnet - if wireguardOnly && wgInterfaceManager != nil { - log.Info().Msg("WireGuard middleware enabled") + // Setup WireGuard middleware if enabled + if wireguardOnly && wireguardServer != "" { wireGuardMiddleware = openchami_middleware.WireGuardMiddlewareWithInterface("wg0", wireguardServer) - } else { - log.Info().Msg("WireGuard middleware disabled") - wireGuardMiddleware = func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) - } } - // Set up router + // Create router router := chi.NewRouter() + + // Add middleware router.Use( middleware.RequestID, middleware.RealIP, @@ -229,25 +221,30 @@ func startServer() error { openchami_logger.OpenCHAMILogger(log.Logger), ) - // Client sub-router - router_client := chi.NewRouter() - initCiClientRouter(router_client, ciHandler, wgInterfaceManager) - router.Mount("/cloud-init", router_client) + // Add WireGuard middleware if enabled + if wireguardOnly && wireGuardMiddleware != nil { + router.Use(wireGuardMiddleware) + } + + // Setup routes + initCiClientRouter(router, handler, nil) + initCiAdminRouter(router, handler) - // Admin sub-router - router_admin := chi.NewRouter() - if secureRouteEnable { - router_admin.Use( + // Add secure routes if JWKS is configured + if secureRouteEnable && keyset != nil { + secureRouter := chi.NewRouter() + secureRouter.Use( jwtauth.Verifier(keyset), openchami_authenticator.AuthenticatorWithRequiredClaims(keyset, []string{"sub", "iss", "aud"}), ) + + // Add secure routes here if needed + router.Mount("/secure", secureRouter) } - initCiAdminRouter(router_admin, ciHandler) - router.Mount("/cloud-init/admin", router_admin) - log.Info().Msgf("Starting server on %s", ciEndpoint) - log.Fatal().Err(http.ListenAndServe(ciEndpoint, router)).Msg("Server closed") - return nil + // Start server + fmt.Printf("Starting cloud-init server on %s\n", ciEndpoint) + return http.ListenAndServe(ciEndpoint, router) } // Utility to read optional environment variables @@ -267,17 +264,24 @@ func initCiClientRouter(router chi.Router, handler *CiHandler, wgInterfaceManage // Add cloud-init endpoints to router router.Get("/openapi.json", DocsHandler) router.Get("/version", VersionHandler) - router.With(wireGuardMiddleware).Get("/user-data", UserDataHandler) - router.With(wireGuardMiddleware).Get("/meta-data", MetaDataHandler(handler.sm, handler.store)) - router.With(wireGuardMiddleware).Get("/vendor-data", VendorDataHandler(handler.sm, handler.store)) - router.With(wireGuardMiddleware).Get("/{group}.yaml", GroupUserDataHandler(handler.sm, handler.store)) + if wireGuardMiddleware != nil { + router.With(wireGuardMiddleware).Get("/user-data", UserDataHandler) + router.With(wireGuardMiddleware).Get("/meta-data", MetaDataHandler(handler.sm, handler.store)) + router.With(wireGuardMiddleware).Get("/vendor-data", VendorDataHandler(handler.sm, handler.store)) + router.With(wireGuardMiddleware).Get("/{group}.yaml", GroupUserDataHandler(handler.sm, handler.store)) + } else { + router.Get("/user-data", UserDataHandler) + router.Get("/meta-data", MetaDataHandler(handler.sm, handler.store)) + router.Get("/vendor-data", VendorDataHandler(handler.sm, handler.store)) + router.Get("/{group}.yaml", GroupUserDataHandler(handler.sm, handler.store)) + } router.Post("/phone-home/{id}", PhoneHomeHandler(wgInterfaceManager, handler.sm)) router.Post("/wg-init", wgtunnel.AddClientHandler(wgInterfaceManager, handler.sm)) } func initCiAdminRouter(router chi.Router, handler *CiHandler) { // admin API subrouter - router.Route("/", func(r chi.Router) { + router.Route("/admin/", func(r chi.Router) { // Cluster Defaults r.Get("/cluster-defaults", GetClusterDataHandler(handler.store)) diff --git a/cmd/cloud-init-server/metadata.go b/cmd/cloud-init-server/metadata.go index e999e6873..a167a865c 100644 --- a/cmd/cloud-init-server/metadata.go +++ b/cmd/cloud-init-server/metadata.go @@ -119,7 +119,7 @@ func generateMetaData(component cistore.OpenCHAMIComponent, groups []string, s c func generateHostname(clusterName string, shortName string, nidLength int, comp cistore.OpenCHAMIComponent) string { // in the future, we might want to map the hostname to an xname or something else. - nid, _ := comp.NID.Int64() + nid, _ := comp.NID.Int64() // We can ignore the error because NID is always set var sname string var nlen int diff --git a/cmd/cloud-init-server/metadata_handlers.go b/cmd/cloud-init-server/metadata_handlers.go index d1bd48881..496c85554 100644 --- a/cmd/cloud-init-server/metadata_handlers.go +++ b/cmd/cloud-init-server/metadata_handlers.go @@ -42,12 +42,12 @@ func getActualRequestIP(r *http.Request) string { // @Failure 422 {object} nil // @Failure 500 {object} nil // @Param id path string false "Node ID" -// @Router /cloud-init/meta-data [get] -// @Router /cloud-init/admin/impersonation/{id}/meta-data [get] +// @Router /meta-data [get] +// @Router /admin/impersonation/{id}/meta-data [get] func MetaDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var urlId string = chi.URLParam(r, "id") - var id = urlId + urlId := chi.URLParam(r, "id") + var id string var err error // If this request includes an id, it can be interrpreted as an impersonation request if urlId == "" { @@ -100,6 +100,8 @@ func MetaDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) http http.Error(w, "Failed to encode metadata to YAML", http.StatusInternalServerError) return } - w.Write(yamlData) + if _, err = w.Write(yamlData); err != nil { + log.Error().Err(err).Msg("failed to write response") + } } } diff --git a/cmd/cloud-init-server/userdata_handlers.go b/cmd/cloud-init-server/userdata_handlers.go index 389e9fcc3..1dcbef173 100644 --- a/cmd/cloud-init-server/userdata_handlers.go +++ b/cmd/cloud-init-server/userdata_handlers.go @@ -23,11 +23,13 @@ import ( // @Produce plain // @Success 200 {object} string // @Param id path string false "Node ID" -// @Router /cloud-init/user-data [get] -// @Router /cloud-init/admin/impersonation/{id}/user-data [get] +// @Router /user-data [get] +// @Router /admin/impersonation/{id}/user-data [get] func UserDataHandler(w http.ResponseWriter, r *http.Request) { payload := `#cloud-config` - w.Write([]byte(payload)) + if _, err := w.Write([]byte(payload)); err != nil { + log.Error().Err(err).Msg("failed to write response") + } } // GroupUserDataHandler godoc @@ -44,8 +46,8 @@ func UserDataHandler(w http.ResponseWriter, r *http.Request) { // @Failure 500 {object} nil // @Param id path string false "Node ID" // @Param group path string true "Group name" -// @Router /cloud-init/{group}.yaml [get] -// @Router /cloud-init/admin/impersonation/{id}/{group}.yaml [get] +// @Router /{group}.yaml [get] +// @Router /admin/impersonation/{id}/{group}.yaml [get] func GroupUserDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, group, err := getIDAndGroup(r, smd) @@ -62,7 +64,9 @@ func GroupUserDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) data, err := store.GetGroupData(group) if err != nil { log.Err(err).Msgf("No information stored for group %s. returning an empty #cloud-config", group) - w.Write([]byte("#cloud-config")) + if _, err2 := w.Write([]byte("#cloud-config")); err2 != nil { + log.Error().Err(err).Msg("failed to write response") + } return } @@ -78,7 +82,9 @@ func GroupUserDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) data.File.Encoding = "plain" } - w.Write(data.File.Content) + if _, err = w.Write(data.File.Content); err != nil { + log.Error().Err(err).Msg("failed to write response") + } } } diff --git a/cmd/cloud-init-server/vendordata_handlers.go b/cmd/cloud-init-server/vendordata_handlers.go index 673e26577..9b9e0f3b6 100644 --- a/cmd/cloud-init-server/vendordata_handlers.go +++ b/cmd/cloud-init-server/vendordata_handlers.go @@ -22,11 +22,11 @@ import ( // @Produce plain // @Success 200 {string} string // @Param id path string false "Node ID" -// @Router /cloud-init/vendor-data [get] -// @Router /cloud-init/admin/impersonation/{id}/vendor-data [get] +// @Router /vendor-data [get] +// @Router /admin/impersonation/{id}/vendor-data [get] func VendorDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var urlId string = chi.URLParam(r, "id") + urlId := chi.URLParam(r, "id") var baseUrl string var id = urlId var err error @@ -70,6 +70,8 @@ func VendorDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) ht for _, group_name := range groups { payload += fmt.Sprintf("%s/%s.yaml\n", baseUrl, group_name) } - w.Write([]byte(payload)) + if _, err = w.Write([]byte(payload)); err != nil { + log.Error().Err(err).Msg("failed to write response") + } } } diff --git a/cmd/cloud-init-server/version.go b/cmd/cloud-init-server/version.go index 2951942e8..66b7f42ac 100644 --- a/cmd/cloud-init-server/version.go +++ b/cmd/cloud-init-server/version.go @@ -8,6 +8,8 @@ import ( "path/filepath" "runtime" "time" + + "github.com/rs/zerolog/log" ) // GitCommit stores the latest Git commit hash. @@ -104,5 +106,7 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) { BytesAllocated: memstats.HeapAlloc, } - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Error().Err(err).Msg("failed to encode response") + } } diff --git a/docs/docs.go b/docs/docs.go index 14ed03d8c..3426c593c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -19,7 +19,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/cloud-init/admin/cluster-defaults": { + "/admin/cluster-defaults": { "get": { "description": "Get default meta-data values for cluster.", "produces": [ @@ -76,7 +76,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/groups": { + "/admin/groups": { "get": { "description": "Get meta-data and cloud-init config for all groups known to\ncloud-init. Note that group membership is managed outside of\nthe cloud-init service, normally in SMD.", "produces": [ @@ -142,7 +142,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/groups/{id}": { + "/admin/groups/{id}": { "get": { "description": "Get meta-data and cloud-init config for a single group known to\ncloud-init.", "produces": [ @@ -200,7 +200,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/groups/{name}": { + "/admin/groups/{name}": { "put": { "description": "Set meta-data or cloud-init configuration for a specific group,\noverwriting any previous values.\n\nIf successful, a 201 Created status is returned and the\n` + "`" + `Location` + "`" + ` header is set to the new group's groups endpoint,\n` + "`" + `/groups/{group}` + "`" + `. This operation is idempotent and replaces\nany existing content.", "consumes": [ @@ -248,7 +248,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/impersonation/{id}/meta-data": { + "/admin/impersonation/{id}/meta-data": { "get": { "description": "Get meta-data for requesting node based on the requesting IP.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nmeta-data will be retrieved for the requested ID.", "produces": [ @@ -282,7 +282,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/impersonation/{id}/user-data": { + "/admin/impersonation/{id}/user-data": { "get": { "description": "Get user-data for requesting node base on the requesting IP. For\nOpenCHAMI, this will always be ` + "`" + `#cloud-config` + "`" + `.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nuser-data will be retrieved for the requested ID.", "produces": [ @@ -307,7 +307,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/impersonation/{id}/vendor-data": { + "/admin/impersonation/{id}/vendor-data": { "get": { "description": "For OpenCHAMI, the vendor-data will always be a list of other\n` + "`" + `#cloud-config` + "`" + ` URLs to download and merge.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nvendor-data will be retrieved for the requested ID.", "produces": [ @@ -332,7 +332,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/impersonation/{id}/{group}.yaml": { + "/admin/impersonation/{id}/{group}.yaml": { "get": { "description": "Get user-data for a particular group based on its name.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\ngroup user-data will be retrieved for the requested ID.", "produces": [ @@ -370,7 +370,7 @@ const docTemplate = `{ } } }, - "/cloud-init/admin/instance-info/{id}": { + "/admin/instance-info/{id}": { "put": { "description": "Set meta-data for a specific node ID, overwriting relevant group meta-data.", "consumes": [ @@ -412,7 +412,7 @@ const docTemplate = `{ } } }, - "/cloud-init/meta-data": { + "/meta-data": { "get": { "description": "Get meta-data for requesting node based on the requesting IP.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nmeta-data will be retrieved for the requested ID.", "produces": [ @@ -438,7 +438,7 @@ const docTemplate = `{ } } }, - "/cloud-init/openapi.json": { + "/openapi.json": { "get": { "produces": [ "application/json" @@ -457,7 +457,7 @@ const docTemplate = `{ } } }, - "/cloud-init/phone-home/{id}": { + "/phone-home/{id}": { "post": { "description": "Signal to the cloud-init server that the specific host has completed running\nthe cloud-init configuration tasks so that, if a WireGuard tunnel is being used,\nit can be torn down. This endpoint should not be manually requested by a user\nbut is only meant to be used by a cloud-init client that has received its\nconfig from an OpenCHAMI cloud-init server.", "tags": [ @@ -525,7 +525,7 @@ const docTemplate = `{ } } }, - "/cloud-init/user-data": { + "/user-data": { "get": { "description": "Get user-data for requesting node base on the requesting IP. For\nOpenCHAMI, this will always be ` + "`" + `#cloud-config` + "`" + `.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nuser-data will be retrieved for the requested ID.", "produces": [ @@ -542,7 +542,7 @@ const docTemplate = `{ } } }, - "/cloud-init/vendor-data": { + "/vendor-data": { "get": { "description": "For OpenCHAMI, the vendor-data will always be a list of other\n` + "`" + `#cloud-config` + "`" + ` URLs to download and merge.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\nvendor-data will be retrieved for the requested ID.", "produces": [ @@ -559,7 +559,7 @@ const docTemplate = `{ } } }, - "/cloud-init/wg-init": { + "/wg-init": { "post": { "description": "Initiate a WireGuard tunnel from a client using its public key\nand peer name (IP address).\n\nThe source IP of the request is read and is used as the peer\nname along with the public key to authenticate unless the\n` + "`" + `X-Forward-For` + "`" + ` header is set. In that case, the value of the\nheader is used as the peer name. If the peer exists in the\ninternal tunnel manager, the IP presented is the one used.\nOtherwise, the next available IP in range is assigned.", "consumes": [ @@ -602,7 +602,7 @@ const docTemplate = `{ } } }, - "/cloud-init/{group}.yaml": { + "/{group}.yaml": { "get": { "description": "Get user-data for a particular group based on its name.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using ` + "`" + `/admin/impersonation` + "`" + `. In this case, the\ngroup user-data will be retrieved for the requested ID.", "produces": [ @@ -718,6 +718,12 @@ const docTemplate = `{ "name": { "type": "string", "example": "compute" + }, + "versions": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 4f30b1668..da983b242 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -11,7 +11,7 @@ "version": "1.0.0" }, "paths": { - "/cloud-init/admin/cluster-defaults": { + "/admin/cluster-defaults": { "get": { "description": "Get default meta-data values for cluster.", "produces": [ @@ -68,7 +68,7 @@ } } }, - "/cloud-init/admin/groups": { + "/admin/groups": { "get": { "description": "Get meta-data and cloud-init config for all groups known to\ncloud-init. Note that group membership is managed outside of\nthe cloud-init service, normally in SMD.", "produces": [ @@ -134,7 +134,7 @@ } } }, - "/cloud-init/admin/groups/{id}": { + "/admin/groups/{id}": { "get": { "description": "Get meta-data and cloud-init config for a single group known to\ncloud-init.", "produces": [ @@ -192,7 +192,7 @@ } } }, - "/cloud-init/admin/groups/{name}": { + "/admin/groups/{name}": { "put": { "description": "Set meta-data or cloud-init configuration for a specific group,\noverwriting any previous values.\n\nIf successful, a 201 Created status is returned and the\n`Location` header is set to the new group's groups endpoint,\n`/groups/{group}`. This operation is idempotent and replaces\nany existing content.", "consumes": [ @@ -240,7 +240,7 @@ } } }, - "/cloud-init/admin/impersonation/{id}/meta-data": { + "/admin/impersonation/{id}/meta-data": { "get": { "description": "Get meta-data for requesting node based on the requesting IP.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nmeta-data will be retrieved for the requested ID.", "produces": [ @@ -274,7 +274,7 @@ } } }, - "/cloud-init/admin/impersonation/{id}/user-data": { + "/admin/impersonation/{id}/user-data": { "get": { "description": "Get user-data for requesting node base on the requesting IP. For\nOpenCHAMI, this will always be `#cloud-config`.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nuser-data will be retrieved for the requested ID.", "produces": [ @@ -299,7 +299,7 @@ } } }, - "/cloud-init/admin/impersonation/{id}/vendor-data": { + "/admin/impersonation/{id}/vendor-data": { "get": { "description": "For OpenCHAMI, the vendor-data will always be a list of other\n`#cloud-config` URLs to download and merge.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nvendor-data will be retrieved for the requested ID.", "produces": [ @@ -324,7 +324,7 @@ } } }, - "/cloud-init/admin/impersonation/{id}/{group}.yaml": { + "/admin/impersonation/{id}/{group}.yaml": { "get": { "description": "Get user-data for a particular group based on its name.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\ngroup user-data will be retrieved for the requested ID.", "produces": [ @@ -362,7 +362,7 @@ } } }, - "/cloud-init/admin/instance-info/{id}": { + "/admin/instance-info/{id}": { "put": { "description": "Set meta-data for a specific node ID, overwriting relevant group meta-data.", "consumes": [ @@ -404,7 +404,7 @@ } } }, - "/cloud-init/meta-data": { + "/meta-data": { "get": { "description": "Get meta-data for requesting node based on the requesting IP.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nmeta-data will be retrieved for the requested ID.", "produces": [ @@ -430,7 +430,7 @@ } } }, - "/cloud-init/openapi.json": { + "/openapi.json": { "get": { "produces": [ "application/json" @@ -449,7 +449,7 @@ } } }, - "/cloud-init/phone-home/{id}": { + "/phone-home/{id}": { "post": { "description": "Signal to the cloud-init server that the specific host has completed running\nthe cloud-init configuration tasks so that, if a WireGuard tunnel is being used,\nit can be torn down. This endpoint should not be manually requested by a user\nbut is only meant to be used by a cloud-init client that has received its\nconfig from an OpenCHAMI cloud-init server.", "tags": [ @@ -517,7 +517,7 @@ } } }, - "/cloud-init/user-data": { + "/user-data": { "get": { "description": "Get user-data for requesting node base on the requesting IP. For\nOpenCHAMI, this will always be `#cloud-config`.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nuser-data will be retrieved for the requested ID.", "produces": [ @@ -534,7 +534,7 @@ } } }, - "/cloud-init/vendor-data": { + "/vendor-data": { "get": { "description": "For OpenCHAMI, the vendor-data will always be a list of other\n`#cloud-config` URLs to download and merge.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\nvendor-data will be retrieved for the requested ID.", "produces": [ @@ -551,7 +551,7 @@ } } }, - "/cloud-init/wg-init": { + "/wg-init": { "post": { "description": "Initiate a WireGuard tunnel from a client using its public key\nand peer name (IP address).\n\nThe source IP of the request is read and is used as the peer\nname along with the public key to authenticate unless the\n`X-Forward-For` header is set. In that case, the value of the\nheader is used as the peer name. If the peer exists in the\ninternal tunnel manager, the IP presented is the one used.\nOtherwise, the next available IP in range is assigned.", "consumes": [ @@ -594,7 +594,7 @@ } } }, - "/cloud-init/{group}.yaml": { + "/{group}.yaml": { "get": { "description": "Get user-data for a particular group based on its name.\n\nIf the impersonation API is enabled, an ID can be provided in\nthe URL path using `/admin/impersonation`. In this case, the\ngroup user-data will be retrieved for the requested ID.", "produces": [ @@ -710,6 +710,12 @@ "name": { "type": "string", "example": "compute" + }, + "versions": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 169f729ae..6bfecceb4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -59,6 +59,10 @@ definitions: name: example: compute type: string + versions: + additionalProperties: + type: string + type: object type: object cistore.OpenCHAMIInstanceInfo: properties: @@ -202,7 +206,7 @@ info: title: OpenCHAMI Cloud-Init Server API version: 1.0.0 paths: - /cloud-init/{group}.yaml: + /{group}.yaml: get: description: |- Get user-data for a particular group based on its name. @@ -228,7 +232,7 @@ paths: "500": description: Internal Server Error summary: Get user-data for a particular group - /cloud-init/admin/cluster-defaults: + /admin/cluster-defaults: get: description: Get default meta-data values for cluster. produces: @@ -266,7 +270,7 @@ paths: tags: - admin - cluster-defaults - /cloud-init/admin/groups: + /admin/groups: get: description: |- Get meta-data and cloud-init config for all groups known to @@ -324,7 +328,7 @@ paths: tags: - admin - groups - /cloud-init/admin/groups/{id}: + /admin/groups/{id}: delete: description: Delete a group with its meta-data and cloud-init config. parameters: @@ -365,7 +369,7 @@ paths: tags: - admin - groups - /cloud-init/admin/groups/{name}: + /admin/groups/{name}: put: consumes: - application/json @@ -404,7 +408,7 @@ paths: tags: - admin - groups - /cloud-init/admin/impersonation/{id}/{group}.yaml: + /admin/impersonation/{id}/{group}.yaml: get: description: |- Get user-data for a particular group based on its name. @@ -434,7 +438,7 @@ paths: "500": description: Internal Server Error summary: Get user-data for a particular group - /cloud-init/admin/impersonation/{id}/meta-data: + /admin/impersonation/{id}/meta-data: get: description: |- Get meta-data for requesting node based on the requesting IP. @@ -461,7 +465,7 @@ paths: "500": description: Internal Server Error summary: Get meta-data for requesting node - /cloud-init/admin/impersonation/{id}/user-data: + /admin/impersonation/{id}/user-data: get: description: |- Get user-data for requesting node base on the requesting IP. For @@ -483,7 +487,7 @@ paths: schema: type: string summary: Get user-data for requesting node - /cloud-init/admin/impersonation/{id}/vendor-data: + /admin/impersonation/{id}/vendor-data: get: description: |- For OpenCHAMI, the vendor-data will always be a list of other @@ -505,7 +509,7 @@ paths: schema: type: string summary: Get vendor data - /cloud-init/admin/instance-info/{id}: + /admin/instance-info/{id}: put: consumes: - application/json @@ -534,7 +538,7 @@ paths: tags: - admin - instance-data - /cloud-init/meta-data: + /meta-data: get: description: |- Get meta-data for requesting node based on the requesting IP. @@ -556,7 +560,7 @@ paths: "500": description: Internal Server Error summary: Get meta-data for requesting node - /cloud-init/openapi.json: + /openapi.json: get: produces: - application/json @@ -568,7 +572,7 @@ paths: "500": description: Internal Server Error summary: Return JSON-formatted OpenAPI documentation - /cloud-init/phone-home/{id}: + /phone-home/{id}: post: description: |- Signal to the cloud-init server that the specific host has completed running @@ -621,7 +625,7 @@ paths: configuration tags: - phone-home - /cloud-init/user-data: + /user-data: get: description: |- Get user-data for requesting node base on the requesting IP. For @@ -638,7 +642,7 @@ paths: schema: type: string summary: Get user-data for requesting node - /cloud-init/vendor-data: + /vendor-data: get: description: |- For OpenCHAMI, the vendor-data will always be a list of other @@ -655,7 +659,7 @@ paths: schema: type: string summary: Get vendor data - /cloud-init/wg-init: + /wg-init: post: consumes: - application/json diff --git a/go.mod b/go.mod index ee7e118aa..8d1b5f6be 100644 --- a/go.mod +++ b/go.mod @@ -4,24 +4,26 @@ go 1.24.0 require ( github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18 - github.com/OpenCHAMI/quack v0.0.1 + github.com/OpenCHAMI/quack v0.0.3 github.com/OpenCHAMI/smd/v2 v2.18.0 - github.com/go-chi/chi/v5 v5.2.1 - github.com/lestrrat-go/jwx/v2 v2.1.5 + github.com/go-chi/chi/v5 v5.2.3 + github.com/lestrrat-go/jwx/v2 v2.1.6 + github.com/marcboeker/go-duckdb v1.8.5 github.com/openchami/chi-middleware/auth v0.0.0-20240812224658-b16b83c70700 github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700 github.com/rs/zerolog v1.34.0 - github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.10.0 - github.com/swaggo/swag v1.16.4 - gopkg.in/yaml.v3 v3.0.1 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.7 + github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.11.1 + github.com/swaggo/swag v1.16.6 ) require ( github.com/Cray-HPE/hms-base/v2 v2.3.0 // indirect github.com/Cray-HPE/hms-xname v1.4.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/apache/arrow-go/v18 v18.2.0 // indirect + github.com/apache/arrow-go/v18 v18.4.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect @@ -30,21 +32,20 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lestrrat-go/blackmagic v1.0.3 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/marcboeker/go-duckdb v1.8.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect @@ -61,10 +62,11 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( @@ -86,12 +88,10 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.1 - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.11.0 // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 30af4b3f0..6635a2168 100644 --- a/go.sum +++ b/go.sum @@ -12,16 +12,16 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18 h1:oBPtXp9RVm9lk5zTmDLf+Vh21yDHpulBxUqGJQjwQCk= github.com/OpenCHAMI/jwtauth/v5 v5.0.0-20240321222802-e6cb468a2a18/go.mod h1:ggNHWgLfW/WRXcE8ZZC4S7UwHif16HVmyowOCWdNSN8= -github.com/OpenCHAMI/quack v0.0.1 h1:qOXyXskLiqMOZqaDSEchUJPPe1qDuC2wUG3kNyyHeDI= -github.com/OpenCHAMI/quack v0.0.1/go.mod h1:5oWhAgheSMM+h713DIJEQlcT+ooOyYwj3z/1WSWWvyM= +github.com/OpenCHAMI/quack v0.0.3 h1:Fov8t+t64hFtBIyU0rPILZ6N8bJ6sWqZ0Sg956/3WcM= +github.com/OpenCHAMI/quack v0.0.3/go.mod h1:7vGD4KHpFhQKR8G6mFXFZHhfhaP4c/V7sUXWImTXChI= github.com/OpenCHAMI/smd/v2 v2.18.0 h1:RZ7lq+zkjZnrnc3McLJANnvoU/T7P8wcCFEukWp0FlQ= github.com/OpenCHAMI/smd/v2 v2.18.0/go.mod h1:RtFmMtTYmMUc74gw4FnfZKDpFMTQJeemAVXB0O+Qaa0= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q= -github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc= -github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= -github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apache/arrow-go/v18 v18.4.0 h1:/RvkGqH517iY8bZKc4FD5/kkdwXJGjxf28JIXbJ/oB0= +github.com/apache/arrow-go/v18 v18.4.0/go.mod h1:Aawvwhj8x2jURIzD9Moy72cF0FyJXOpkYpdmGRHcw14= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -39,8 +39,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= @@ -53,13 +53,13 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -100,8 +100,8 @@ github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -114,8 +114,8 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.5 h1:PQI5gzadLfJ22ckLrejPVX6eGXKM4M4eGi5fW2jjA3o= -github.com/lestrrat-go/jwx/v2 v2.1.5/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -172,20 +172,21 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= -github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -194,33 +195,33 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= -gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/duckdbstore/ciDuckDBStore.go b/internal/duckdbstore/ciDuckDBStore.go new file mode 100644 index 000000000..227b88ee5 --- /dev/null +++ b/internal/duckdbstore/ciDuckDBStore.go @@ -0,0 +1,191 @@ +package duckdbstore + +import ( + "database/sql" + "encoding/json" + "fmt" + "sync" + + "github.com/OpenCHAMI/cloud-init/pkg/cistore" + _ "github.com/marcboeker/go-duckdb" +) + +type DuckDBStore struct { + db *sql.DB + mu sync.RWMutex +} + +func NewDuckDBStore(dsn string) (*DuckDBStore, error) { + db, err := sql.Open("duckdb", dsn) + if err != nil { + return nil, err + } + return &DuckDBStore{db: db}, nil +} + +func (d *DuckDBStore) GetGroups() (map[string]cistore.GroupData, error) { + d.mu.RLock() + defer d.mu.RUnlock() + rows, err := d.db.Query("SELECT name, description, data, file, versions FROM groups") + if err != nil { + return nil, err + } + defer func() { + _ = rows.Close() // Ignoring error on deferred Close + }() + + groups := make(map[string]cistore.GroupData) + for rows.Next() { + var group cistore.GroupData + var data, file, versions []byte + if err := rows.Scan(&group.Name, &group.Description, &data, &file, &versions); err != nil { + continue + } + err := json.Unmarshal(data, &group.Data) + if err != nil { + return nil, err + } + group.File.Content = file + if err = json.Unmarshal(versions, &group.Versions); err != nil { + return nil, err + } + groups[group.Name] = group + } + return groups, nil +} + +func (d *DuckDBStore) AddGroupData(groupName string, groupData cistore.GroupData) error { + d.mu.Lock() + defer d.mu.Unlock() + data, _ := json.Marshal(groupData.Data) // Ignoring error because Data is always serializable + versions, _ := json.Marshal(groupData.Versions) // Ignoring error because Versions is always serializable + _, err := d.db.Exec("INSERT INTO groups (name, description, data, file, versions) VALUES (?, ?, ?, ?, ?)", + groupName, groupData.Description, data, groupData.File.Content, versions) + return err +} + +func (d *DuckDBStore) GetGroupData(groupName string) (cistore.GroupData, error) { + d.mu.RLock() + defer d.mu.RUnlock() + var group cistore.GroupData + var data, file, versions []byte + err := d.db.QueryRow("SELECT name, description, data, file, versions FROM groups WHERE name = ?", groupName). + Scan(&group.Name, &group.Description, &data, &file, &versions) + if err != nil { + return group, err + } + err = json.Unmarshal(data, &group.Data) + if err != nil { + return group, err + } + group.File.Content = file + err = json.Unmarshal(versions, &group.Versions) + if err != nil { + return group, err + } + return group, nil +} + +func (d *DuckDBStore) UpdateGroupData(groupName string, groupData cistore.GroupData, create bool) error { + d.mu.Lock() + defer d.mu.Unlock() + data, _ := json.Marshal(groupData.Data) // Ignoring error because Data is always serializable + versions, _ := json.Marshal(groupData.Versions) // Ignoring error because Versions is always serializable + if create { + _, err := d.db.Exec("INSERT INTO groups (name, description, data, file, versions) VALUES (?, ?, ?, ?, ?)", + groupName, groupData.Description, data, groupData.File.Content, versions) + return err + } + _, err := d.db.Exec("UPDATE groups SET description = ?, data = ?, file = ?, versions = ? WHERE name = ?", + groupData.Description, data, groupData.File.Content, versions, groupName) + return err +} + +func (d *DuckDBStore) RemoveGroupData(groupName string) error { + d.mu.Lock() + defer d.mu.Unlock() + _, err := d.db.Exec("DELETE FROM groups WHERE name = ?", groupName) + return err +} + +func (d *DuckDBStore) GetInstanceInfo(nodeName string) (cistore.OpenCHAMIInstanceInfo, error) { + d.mu.RLock() + defer d.mu.RUnlock() + var instance cistore.OpenCHAMIInstanceInfo + err := d.db.QueryRow("SELECT id, instance_id, local_hostname, hostname, cluster_name, region, availability_zone, cloud_provider, instance_type, cloud_init_base_url, public_keys FROM instances WHERE id = ?", nodeName). + Scan(&instance.ID, &instance.InstanceID, &instance.LocalHostname, &instance.Hostname, &instance.ClusterName, &instance.Region, &instance.AvailabilityZone, &instance.CloudProvider, &instance.InstanceType, &instance.CloudInitBaseURL, &instance.PublicKeys) + return instance, err +} + +func (d *DuckDBStore) SetInstanceInfo(nodeName string, instanceInfo cistore.OpenCHAMIInstanceInfo) error { + d.mu.Lock() + defer d.mu.Unlock() + publicKeys, _ := json.Marshal(instanceInfo.PublicKeys) // Not checking error because PublicKeys is always serializable + _, err := d.db.Exec("INSERT INTO instances (id, instance_id, local_hostname, hostname, cluster_name, region, availability_zone, cloud_provider, instance_type, cloud_init_base_url, public_keys) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET instance_id = ?, local_hostname = ?, hostname = ?, cluster_name = ?, region = ?, availability_zone = ?, cloud_provider = ?, instance_type = ?, cloud_init_base_url = ?, public_keys = ?", + nodeName, instanceInfo.InstanceID, instanceInfo.LocalHostname, instanceInfo.Hostname, instanceInfo.ClusterName, instanceInfo.Region, instanceInfo.AvailabilityZone, instanceInfo.CloudProvider, instanceInfo.InstanceType, instanceInfo.CloudInitBaseURL, publicKeys, + instanceInfo.InstanceID, instanceInfo.LocalHostname, instanceInfo.Hostname, instanceInfo.ClusterName, instanceInfo.Region, instanceInfo.AvailabilityZone, instanceInfo.CloudProvider, instanceInfo.InstanceType, instanceInfo.CloudInitBaseURL, publicKeys) + return err +} + +func (d *DuckDBStore) DeleteInstanceInfo(nodeName string) error { + d.mu.Lock() + defer d.mu.Unlock() + _, err := d.db.Exec("DELETE FROM instances WHERE id = ?", nodeName) + return err +} + +func (d *DuckDBStore) GetClusterDefaults() (cistore.ClusterDefaults, error) { + d.mu.RLock() + defer d.mu.RUnlock() + var defaults cistore.ClusterDefaults + err := d.db.QueryRow("SELECT cloud_provider, region, availability_zone, cluster_name, public_keys, base_url, boot_subnet, wg_subnet, short_name, nid_length FROM cluster_defaults"). + Scan(&defaults.CloudProvider, &defaults.Region, &defaults.AvailabilityZone, &defaults.ClusterName, &defaults.PublicKeys, &defaults.BaseUrl, &defaults.BootSubnet, &defaults.WGSubnet, &defaults.ShortName, &defaults.NidLength) + return defaults, err +} + +func (d *DuckDBStore) SetClusterDefaults(clusterDefaults cistore.ClusterDefaults) error { + d.mu.Lock() + defer d.mu.Unlock() + publicKeys, _ := json.Marshal(clusterDefaults.PublicKeys) // Not checking error on Marshall because PublicKeys is always serializable + _, err := d.db.Exec("INSERT INTO cluster_defaults (cloud_provider, region, availability_zone, cluster_name, public_keys, base_url, boot_subnet, wg_subnet, short_name, nid_length) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET cloud_provider = ?, region = ?, availability_zone = ?, cluster_name = ?, public_keys = ?, base_url = ?, boot_subnet = ?, wg_subnet = ?, short_name = ?, nid_length = ?", + clusterDefaults.CloudProvider, clusterDefaults.Region, clusterDefaults.AvailabilityZone, clusterDefaults.ClusterName, publicKeys, clusterDefaults.BaseUrl, clusterDefaults.BootSubnet, clusterDefaults.WGSubnet, clusterDefaults.ShortName, clusterDefaults.NidLength, + clusterDefaults.CloudProvider, clusterDefaults.Region, clusterDefaults.AvailabilityZone, clusterDefaults.ClusterName, publicKeys, clusterDefaults.BaseUrl, clusterDefaults.BootSubnet, clusterDefaults.WGSubnet, clusterDefaults.ShortName, clusterDefaults.NidLength) + return err +} + +func (d *DuckDBStore) SerializeToParquet(filePath string) error { + d.mu.Lock() + defer d.mu.Unlock() + tables := []string{"groups", "instances", "cluster_defaults"} + for _, table := range tables { + _, err := d.db.Exec(fmt.Sprintf("COPY (SELECT * FROM %s) TO '%s/%s.parquet' (FORMAT 'parquet')", table, filePath, table)) + if err != nil { + return err + } + } + return nil +} + +func (d *DuckDBStore) LoadFromParquet(filePath string) error { + d.mu.Lock() + defer d.mu.Unlock() + tables := []string{"groups", "instances", "cluster_defaults"} + for _, table := range tables { + _, err := d.db.Exec(fmt.Sprintf("COPY %s FROM '%s/%s.parquet' (FORMAT 'parquet')", table, filePath, table)) + if err != nil { + return err + } + } + return nil +} + +func (d *DuckDBStore) ApplyMigrations(migrations []string) error { + d.mu.Lock() + defer d.mu.Unlock() + for _, migration := range migrations { + if _, err := d.db.Exec(migration); err != nil { + return err + } + } + return nil +} diff --git a/internal/memstore/ciMemStore.go b/internal/memstore/ciMemStore.go index 1aeb0e85f..f4bea30c5 100644 --- a/internal/memstore/ciMemStore.go +++ b/internal/memstore/ciMemStore.go @@ -180,6 +180,6 @@ func generateInstanceId() string { func generateUniqueID(prefix string) string { b := make([]byte, 4) - rand.Read(b) + _, _ = rand.Read(b) // Read fills b with cryptographically secure random bytes. It never returns an error, and always fills b entirely return fmt.Sprintf("%s-%x", prefix, b) } diff --git a/internal/memstore/ciMemStore_test.go b/internal/memstore/ciMemStore_test.go new file mode 100644 index 000000000..fde785ef8 --- /dev/null +++ b/internal/memstore/ciMemStore_test.go @@ -0,0 +1,20 @@ +package memstore + +import ( + "testing" + + storetesting "github.com/OpenCHAMI/cloud-init/pkg/cistore/testing" +) + +func TestMemStore(t *testing.T) { + // Create a new MemStore instance + store := NewMemStore() + + // Create a cleanup function that will be called in all cases + cleanup := func() { + // No cleanup needed for MemStore as it's in-memory + } + + // Run the standard test suite + storetesting.RunStoreTests(t, store, cleanup) +} diff --git a/internal/middleware/wireguard.go b/internal/middleware/wireguard.go index 99267ea1c..03996c296 100644 --- a/internal/middleware/wireguard.go +++ b/internal/middleware/wireguard.go @@ -157,7 +157,7 @@ func WireGuardMiddlewareWithInterface(wireGuardInterface string, wireGuardCIDR s return } for _, iface := range interfaces { - addrs, _ := iface.Addrs() + addrs, _ := iface.Addrs() // Ignoring error on Addrs() as we can still check other interfaces for _, ifaceAddr := range addrs { if ipNet, ok := ifaceAddr.(*net.IPNet); ok && ipNet.IP.Equal(ip) { recievedInterface = iface diff --git a/internal/quackstore/quackstore.go b/internal/quackstore/quackstore.go index 0006fc779..0ecd14c5d 100644 --- a/internal/quackstore/quackstore.go +++ b/internal/quackstore/quackstore.go @@ -1,6 +1,7 @@ package quackstore import ( + "crypto/rand" "database/sql" "encoding/json" "fmt" @@ -67,7 +68,9 @@ func (s *QuackStore) GetGroups() map[string]cistore.GroupData { fmt.Printf("Error querying groups: %v\n", err) return groups } - defer rows.Close() + defer func() { + _ = rows.Close() // Ignoring error on deferred Close + }() for rows.Next() { var name string @@ -348,8 +351,10 @@ func (s *QuackStore) Close() error { return s.db.Close() } -// generateInstanceId generates a unique instance ID +// generateInstanceID generates a unique instance ID in the format "i-XXXXXX", +// where "XXXXXX" is a random 6-digit hexadecimal string. func generateInstanceId() string { - // TODO: Implement proper instance ID generation - return "i-1234567890abcdef" + randBytes := make([]byte, 3) + _, _ = rand.Read(randBytes) // Read fills randBytes with cryptographically secure random bytes. It never returns an error, and always fills randBytes entirely + return fmt.Sprintf("i-%x", randBytes) } diff --git a/internal/quackstore/quackstore_test.go b/internal/quackstore/quackstore_test.go index 23cc7ef82..827aea144 100644 --- a/internal/quackstore/quackstore_test.go +++ b/internal/quackstore/quackstore_test.go @@ -16,7 +16,9 @@ func TestQuackStore(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { + _ = os.RemoveAll(tmpDir) // Clean up the temp directory. Ignoring error on RemoveAll + }() // Create the database file path dbPath := filepath.Join(tmpDir, "test.db") @@ -25,7 +27,9 @@ func TestQuackStore(t *testing.T) { t.Run("Database Initialization", func(t *testing.T) { store, err := NewQuackStore(dbPath) assert.NoError(t, err) - defer store.Close() + defer func() { + _ = store.Close() // Ignoring error on deferred Close + }() // Verify database file was created _, err = os.Stat(dbPath) @@ -45,11 +49,13 @@ func TestQuackStore(t *testing.T) { // Test database cleanup t.Run("Database Cleanup", func(t *testing.T) { // Remove the existing database file to start fresh - os.Remove(dbPath) + _ = os.Remove(dbPath) // Ignoring error on Remove store, err := NewQuackStore(dbPath) assert.NoError(t, err) - defer store.Close() + defer func() { + _ = store.Close() // Ignoring error on deferred Close + }() // Insert some test data _, err = store.db.Exec("INSERT INTO groups (name, data) VALUES (?, ?)", "cleanup-test", "{}") @@ -62,7 +68,9 @@ func TestQuackStore(t *testing.T) { // Reopen the database and verify data persists store, err = NewQuackStore(dbPath) assert.NoError(t, err) - defer store.Close() + defer func() { + _ = store.Close() // Ignoring error on deferred Close + }() var count int err = store.db.QueryRow("SELECT COUNT(*) FROM groups").Scan(&count) @@ -71,19 +79,21 @@ func TestQuackStore(t *testing.T) { }) // Remove the database file before running the standard test suite - os.Remove(dbPath) + _ = os.Remove(dbPath) // Ignoring error on Remove // Create a new QuackStore instance for the standard test suite store, err := NewQuackStore(dbPath) if err != nil { t.Fatalf("Failed to create QuackStore: %v", err) } - defer store.Close() + defer func() { + _ = store.Close() // Ignoring error on deferred Close + }() // Create a cleanup function that will be called in all cases cleanup := func() { - store.Close() - os.RemoveAll(tmpDir) + _ = store.Close() // Ignoring error on deferred Close + _ = os.RemoveAll(tmpDir) // Ignoring error on RemoveAll } defer cleanup() diff --git a/internal/smdclient/FakeSMDClient.go b/internal/smdclient/FakeSMDClient.go index 2f12b71f9..e3f6d6b88 100644 --- a/internal/smdclient/FakeSMDClient.go +++ b/internal/smdclient/FakeSMDClient.go @@ -123,7 +123,10 @@ func (f *FakeSMDClient) ComponentInformation(id string) (base.Component, error) log.Debug().Msgf("FakeSMDClient: %d components from %s to %s", len(f.components), f.rosetta_mapping[0].ComponentID, f.rosetta_mapping[len(f.rosetta_mapping)-1].ComponentID) if c, ok := f.components[id]; ok { log.Debug().Msgf("FakeSMDClient: ComponentInformation(%s) found", id) - groups, _ := f.GroupMembership(id) + groups, err := f.GroupMembership(id) + if err != nil { + log.Error().Err(err).Msg("failed to get group membership by ID") + } log.Debug().Msgf("FakeSMDClient: Groups for %s found: %v", id, groups) return c, nil } @@ -251,7 +254,7 @@ func generateFakeComponents(numComponents int, cidr string) (map[string]base.Com for i := 1; i <= numComponents; i++ { mac = incrementMAC(mac) - xname, _ = incrementXname(xname) + xname, _ = incrementXname(xname) // ignoring error because we know the format is correct component := base.Component{ ID: xname, Type: "Node", diff --git a/internal/smdclient/FakeSMDClient_handlers.go b/internal/smdclient/FakeSMDClient_handlers.go index 7503d7df7..4a57a938e 100644 --- a/internal/smdclient/FakeSMDClient_handlers.go +++ b/internal/smdclient/FakeSMDClient_handlers.go @@ -46,7 +46,11 @@ func ListNodesHandler(f *FakeSMDClient) http.HandlerFunc { nodes := f.ListNodes() w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(nodes) + if err := json.NewEncoder(w).Encode(nodes); err != nil { + log.Error().Err(err).Msg("Failed to encode nodes") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } } diff --git a/internal/smdclient/SMDclient.go b/internal/smdclient/SMDclient.go index 58cb4228a..505cc38bb 100644 --- a/internal/smdclient/SMDclient.go +++ b/internal/smdclient/SMDclient.go @@ -72,10 +72,12 @@ type NodeMapping struct { // and uses the provided JWT server for authentication func NewSMDClient(clusterName, baseurl, jwtURL, accessToken, certPath string, insecure bool) (*SMDClient, error) { var ( - c *http.Client = &http.Client{Timeout: 2 * time.Second} + c *http.Client certPool *x509.CertPool ) + c = &http.Client{Timeout: 2 * time.Second} + // try and load the cert if path is provided first if certPath != "" { cacert, err := os.ReadFile(certPath) @@ -180,7 +182,15 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { log.Info().Msg("Cached JWT was rejected by SMD") if !freshToken { log.Info().Msg("Fetching new JWT and retrying...") - s.RefreshToken() + // Try to refresh the token and retry once + if err2 := s.RefreshToken(); err2 != nil { + // If token refresh fails, refresh will attempt again. + // While effectively we could ignore the error, it helps + // to see why the failure is occurring in case the error + // is unusual (RefreshToken() has a few different failure + // modes). + log.Debug().Err(err).Msg("failed to refresh token") + } freshToken = true } else { log.Info().Msg("SMD authentication failed, even with a fresh" + @@ -193,7 +203,9 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { break } } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() // ignoring error on deferred Close + }() body, err := io.ReadAll(resp.Body) if err != nil { log.Error().Err(err).Msg("failed to read response body") diff --git a/internal/smdclient/SMDclient_test.go b/internal/smdclient/SMDclient_test.go index 493be927f..9fe046175 100644 --- a/internal/smdclient/SMDclient_test.go +++ b/internal/smdclient/SMDclient_test.go @@ -17,7 +17,7 @@ func TestPopulateNodes(t *testing.T) { assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`[ + if _, err := w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -48,7 +48,10 @@ func TestPopulateNodes(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)) + ]`)); err != nil { + // If an error occurs here, something is very wrong. + t.Errorf("Write(): unexpected error: %v", err) + } }) server := httptest.NewServer(handler) defer server.Close() @@ -104,7 +107,7 @@ func TestIPfromID(t *testing.T) { assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`[ + if _, err := w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -135,7 +138,10 @@ func TestIPfromID(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)) + ]`)); err != nil { + // If an error occurs here, something is very wrong. + t.Errorf("Write(): unexpected error: %v", err) + } }) server := httptest.NewServer(handler) defer server.Close() @@ -183,7 +189,7 @@ func TestIDfromIP(t *testing.T) { assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`[ + if _, err := w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -214,7 +220,10 @@ func TestIDfromIP(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)) + ]`)); err != nil { + // If an error occurs here, something is very wrong. + t.Errorf("Write(): unexpected error: %v", err) + } }) server := httptest.NewServer(handler) defer server.Close() @@ -262,7 +271,7 @@ func TestIDfromMAC(t *testing.T) { assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(`[ + if _, err := w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -293,7 +302,10 @@ func TestIDfromMAC(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)) + ]`)); err != nil { + // If an error occurs here, something is very wrong. + t.Errorf("Write(): unexpected error: %v", err) + } }) server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/cistore/models.go b/pkg/cistore/models.go index 1c4178af5..629a24633 100644 --- a/pkg/cistore/models.go +++ b/pkg/cistore/models.go @@ -3,18 +3,16 @@ package cistore import ( "encoding/json" "errors" - "fmt" - - "gopkg.in/yaml.v3" base "github.com/Cray-HPE/hms-base" ) type GroupData struct { - Name string `json:"name" yaml:"name" example:"compute" description:"Group name"` - Description string `json:"description,omitempty" yaml:"description,omitempty" example:"The compute group" description:"A short description of the group"` - Data map[string]interface{} `json:"meta-data,omitempty" yaml:"meta-data,omitempty" description:"json map of a string (key) to a struct (value) representing group meta-data"` - File CloudConfigFile `json:"file,omitempty" yaml:"file,omitempty" description:"Cloud-Init configuration for group"` + Name string `json:"name" example:"compute" description:"Group name"` + Description string `json:"description,omitempty" example:"The compute group" description:"A short description of the group"` + Data map[string]interface{} `json:"meta-data,omitempty" description:"json map of a string (key) to a struct (value) representing group meta-data"` + File CloudConfigFile `json:"file,omitempty" description:"Cloud-Init configuration for group"` + Versions map[string]string `json:"versions,omitempty" description:"Map of group versions"` } func (g *GroupData) ParseFromJSON(body []byte) error { @@ -33,13 +31,13 @@ func (g *GroupData) ParseFromJSON(body []byte) error { type OpenCHAMIComponent struct { base.Component - MAC string `json:"mac" yaml:"mac"` // MAC address of the inteface used to boot the component - IP string `json:"ip" yaml:"ip"` // IP address of the interface used to boot the component - WGIP string `json:"wgip,omitempty" yaml:"wgip,omitempty"` // Wireguard IP address of the interface used for cloud-init + MAC string `json:"mac"` // MAC address of the inteface used to boot the component + IP string `json:"ip"` // IP address of the interface used to boot the component + WGIP string `json:"wgip,omitempty"` // Wireguard IP address of the interface used for cloud-init } type OpenCHAMIInstanceInfo struct { - ID string `json:"id" yaml:"id" example:"x3000c1b1n1" description:"Node unique identifier, on systems that support xnames, this will be an xname which includes location information"` + ID string `json:"id" example:"x3000c1b1n1" description:"Node unique identifier, on systems that support xnames, this will be an xname which includes location information"` InstanceID string `json:"instance-id" yaml:"instance-id"` LocalHostname string `json:"local-hostname,omitempty" yaml:"local-hostname" example:"compute-1" description:"Node-specific hostname"` Hostname string `json:"hostname,omitempty" yaml:"hostname"` @@ -68,12 +66,12 @@ type ClusterDefaults struct { } type CloudConfigFile struct { - Content []byte `json:"content" yaml:"content" swaggertype:"string" example:"IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKbWVyZ2VfaG93OgotIG5hbWU6IGxpc3QKICBzZXR0aW5nczogW2FwcGVuZF0KLSBuYW1lOiBkaWN0CiAgc2V0dGluZ3M6IFtub19yZXBsYWNlLCByZWN1cnNlX2xpc3RdCnVzZXJzOgogIC0gbmFtZTogcm9vdAogICAgc3NoX2F1dGhvcml6ZWRfa2V5czoge3sgZHMubWV0YV9kYXRhLmluc3RhbmNlX2RhdGEudjEucHVibGljX2tleXMgfX0KZGlzYWJsZV9yb290OiBmYWxzZQo=" description:"Cloud-Init configuration content whose encoding depends on the value of 'encoding'"` - Name string `json:"filename" yaml:"filename"` - Encoding string `json:"encoding,omitempty" yaml:"encoding,omitempty" enums:"base64,plain"` + Content []byte `json:"content" swaggertype:"string" example:"IyMgdGVtcGxhdGU6IGppbmphCiNjbG91ZC1jb25maWcKbWVyZ2VfaG93OgotIG5hbWU6IGxpc3QKICBzZXR0aW5nczogW2FwcGVuZF0KLSBuYW1lOiBkaWN0CiAgc2V0dGluZ3M6IFtub19yZXBsYWNlLCByZWN1cnNlX2xpc3RdCnVzZXJzOgogIC0gbmFtZTogcm9vdAogICAgc3NoX2F1dGhvcml6ZWRfa2V5czoge3sgZHMubWV0YV9kYXRhLmluc3RhbmNlX2RhdGEudjEucHVibGljX2tleXMgfX0KZGlzYWJsZV9yb290OiBmYWxzZQo=" description:"Cloud-Init configuration content whose encoding depends on the value of 'encoding'"` + Name string `json:"filename"` + Encoding string `json:"encoding,omitempty" enums:"base64,plain"` } -// Custom JSON unmarshaler for CloudConfigFile +// UnmarshalJSON implements json.Unmarshaler func (f *CloudConfigFile) UnmarshalJSON(data []byte) error { // Use an auxiliary struct so that: // @@ -85,7 +83,7 @@ func (f *CloudConfigFile) UnmarshalJSON(data []byte) error { // to f.Content. type Alias CloudConfigFile aux := &struct { - Content string `json:"content" yaml:"content"` + Content string `json:"content"` *Alias }{ Alias: (*Alias)(f), @@ -99,57 +97,11 @@ func (f *CloudConfigFile) UnmarshalJSON(data []byte) error { return nil } -// Custom YAML unmarshaller for CloudConfigFile. This is needed because the yaml -// library cannot unmarshal string to []byte like json can, so it needs to be -// told how to do so. -func (f *CloudConfigFile) UnmarshalYAML(n *yaml.Node) error { - // Use an auxiliary struct so that: - // - // 1. json.Unmarshal doesn't recurse forever and overflow the stack. - // 2. json.Unmarshal doesn't try to base64-decode "content" in the data - // before assigning the bytes to f.Content. Content is unmarshalled - // as a string instead of bytes in order to prevent this. After - // unmarshalling, the string is converted back to bytes and assigned - // to f.Content. - // - // Interestingly, yaml.Unmarshal will not unmarshal into aux's pointer - // to f, so we have to use json.Unmarshal. - type Alias CloudConfigFile - aux := &struct { - Content string `json:"content" yaml:"content"` - *Alias - }{ - Alias: (*Alias)(f), - } - - // Decode YAML document (n) into aux struct, using a map as an - // intermediary (since n.Decode() cannot decode into a []byte). We have - // to use JSON for the unmarshalling (and, by consequence, the - // marshalling) so that f will get written via aux's pointer to it. For - // some reason, the yaml unmarshaller will not do that. - // - // 1. Decode YAML document (n) into map. - // 2. JSON marshal map into bytes. - // 3. JSON unmarshal bytes into aux struct. - // 4. Set f.Content to byte-ified aux.Content. - var mAux map[string]interface{} - if err := n.Decode(&mAux); err != nil { - return err - } - t, err := json.Marshal(mAux) - if err != nil { - return err - } - if err := json.Unmarshal(t, &aux); err != nil { - return err - } - f.Content = []byte(aux.Content) - - return nil -} - -// Custom JSON marshaler for CloudConfigFile +// Custom marshaler for CloudConfigFile func (f CloudConfigFile) MarshalJSON() ([]byte, error) { + // Use temporary struct to marshal so json.Marshal doesn't recurse + // indefinitely. Also to convert Content from bytes to string so + // json.Marshal doesn't try to base64 encode the bytes. // Use an auxiliary struct so that: // // 1. json.Marshal doesn't recurse forever and overflow the stack. @@ -158,60 +110,12 @@ func (f CloudConfigFile) MarshalJSON() ([]byte, error) { // to prevent this. Then, aux gets marshalled instead of f. type Alias CloudConfigFile aux := &struct { - Content string `json:"content" yaml:"content"` + Content string `json:"content"` Alias }{ Alias: (Alias)(f), } - aux.Content = string(f.Content) - return json.Marshal(aux) -} - -// Custom YAML marshaler for CloudConfigFile. This is needed because the yaml -// library will not marshal []byte into string like json will, so it needs to be -// told how to do so. -func (f CloudConfigFile) MarshalYAML() (interface{}, error) { - // Use an auxiliary struct so that: - // - // 1. yaml.Marshal doesn't recurse forever and overflow the stack. - // 2. yaml.Marshal doesn't try to base64-encode f.Content. f.Content is - // converted from bytes to a string and then assigned to aux.Content - // to prevent this. Then, aux gets marshalled instead of f. - // - // aux is set to the values of f, but has its own string Content, set to - // the stringified f.Content. - type Alias CloudConfigFile - aux := &struct { - Content string `yaml:"content"` - Alias - }{ - Content: string(f.Content), - Alias: (Alias)(f), - } - // Convert aux into map, which has an "alias" key containing the - // "content" that will be set to the actual string value. The map that - // is mapped to "alias" is what is returned. - // - // 1. YAML marshal aux to bytes. - // 2. YAML unmarshal bytes into map. - // 3. Set content in "alias" to aux.Content. - // 4. Return "alias" map. - t, err := yaml.Marshal(aux) - if err != nil { - return nil, err - } - var nMap map[string]interface{} - if err := yaml.Unmarshal(t, &nMap); err != nil { - return nil, err - } - switch c := (nMap["alias"]).(type) { - case map[string]interface{}: - c["content"] = aux.Content - default: - return nil, fmt.Errorf("cloud config file in map is unknown type: wanted=(map[string]interface{}) actual=(%v)", c) - } - - return nMap["alias"], nil + return json.Marshal(aux) } diff --git a/pkg/cistore/models_test.go b/pkg/cistore/models_test.go index 10edb1a68..96b2d92ad 100644 --- a/pkg/cistore/models_test.go +++ b/pkg/cistore/models_test.go @@ -3,7 +3,6 @@ package cistore import ( "encoding/base64" "encoding/json" - "gopkg.in/yaml.v3" "testing" "github.com/stretchr/testify/assert" @@ -24,19 +23,6 @@ func TestCloudConfigFile_UnmarshalJSON_Plain(t *testing.T) { assert.Equal(t, []byte("#cloud-config\nusers:\n - name: test"), f.Content) } -func TestCloudConfigFile_UnmarshalYAML_Plain(t *testing.T) { - yamlData := []byte(`filename: myconfig.yaml -encoding: plain -content: "#cloud-config\nusers:\n - name: test"`) - - var f CloudConfigFile - err := yaml.Unmarshal(yamlData, &f) - assert.NoError(t, err) - assert.Equal(t, "myconfig.yaml", f.Name) - assert.Equal(t, "plain", f.Encoding) - assert.Equal(t, []byte("#cloud-config\nusers:\n - name: test"), f.Content) -} - func TestCloudConfigFile_UnmarshalJSON_Base64(t *testing.T) { encodedContent := base64.StdEncoding.EncodeToString([]byte("#cloud-config\nusers:\n - name: test")) jsonData := []byte(`{ @@ -54,20 +40,6 @@ func TestCloudConfigFile_UnmarshalJSON_Base64(t *testing.T) { assert.Equal(t, []byte(encodedContent), f.Content) } -func TestCloudConfigFile_UnmarshalYAML_Base64(t *testing.T) { - encodedContent := base64.StdEncoding.EncodeToString([]byte("#cloud-config\nusers:\n - name: test")) - yamlData := []byte(`filename: myconfig.yaml -encoding: base64 -content: ` + encodedContent) - - var f CloudConfigFile - err := yaml.Unmarshal(yamlData, &f) - assert.NoError(t, err) - assert.Equal(t, "myconfig.yaml", f.Name) - assert.Equal(t, "base64", f.Encoding) - assert.Equal(t, []byte(encodedContent), f.Content) -} - func TestCloudConfigFile_MarshalJSON_Plain(t *testing.T) { f := CloudConfigFile{ Name: "plainconfig.yaml", @@ -86,24 +58,6 @@ func TestCloudConfigFile_MarshalJSON_Plain(t *testing.T) { assert.Equal(t, "#cloud-config\nusers:\n - name: test", out["content"]) } -func TestCloudConfigFile_MarshalYAML_Plain(t *testing.T) { - f := CloudConfigFile{ - Name: "plainconfig.yaml", - Encoding: "plain", - Content: []byte("#cloud-config\nusers:\n - name: test"), - } - - data, err := yaml.Marshal(f) - assert.NoError(t, err) - - var out map[string]interface{} - err = yaml.Unmarshal(data, &out) - assert.NoError(t, err) - assert.Equal(t, "plainconfig.yaml", out["filename"]) - assert.Equal(t, "plain", out["encoding"]) - assert.Equal(t, "#cloud-config\nusers:\n - name: test", out["content"]) -} - func TestCloudConfigFile_MarshalJSON_Base64(t *testing.T) { originalConfig := "#cloud-config\nusers:\n - name: test" b64Config := base64.StdEncoding.EncodeToString([]byte(originalConfig)) @@ -123,23 +77,3 @@ func TestCloudConfigFile_MarshalJSON_Base64(t *testing.T) { assert.Equal(t, "base64", out["encoding"]) assert.Equal(t, b64Config, out["content"]) } - -func TestCloudConfigFile_MarshalYAML_Base64(t *testing.T) { - originalConfig := "#cloud-config\nusers:\n - name: test" - b64Config := base64.StdEncoding.EncodeToString([]byte(originalConfig)) - f := CloudConfigFile{ - Name: "encodedconfig.yaml", - Encoding: "base64", - Content: []byte(b64Config), - } - - data, err := yaml.Marshal(f) - assert.NoError(t, err) - - var out map[string]interface{} - err = yaml.Unmarshal(data, &out) - assert.NoError(t, err) - assert.Equal(t, "encodedconfig.yaml", out["filename"]) - assert.Equal(t, "base64", out["encoding"]) - assert.Equal(t, b64Config, out["content"]) -} diff --git a/pkg/cistore/testing/store_test_suite.go b/pkg/cistore/testing/store_test_suite.go index 2fad4e4b9..5d96f938c 100644 --- a/pkg/cistore/testing/store_test_suite.go +++ b/pkg/cistore/testing/store_test_suite.go @@ -21,7 +21,7 @@ func RunStoreTests(t *testing.T, store cistore.Store, cleanup func()) { // Clear existing data groups := store.GetGroups() for name := range groups { - store.RemoveGroupData(name) + _ = store.RemoveGroupData(name) // Ignoring error on RemoveGroupData as the test will catch any issues } testGroupOperations(t, store) }) @@ -30,7 +30,7 @@ func RunStoreTests(t *testing.T, store cistore.Store, cleanup func()) { // Clear existing data groups := store.GetGroups() for name := range groups { - store.RemoveGroupData(name) + _ = store.RemoveGroupData(name) // Ignoring error on RemoveGroupData as the test will catch any issues } testInstanceOperations(t, store) }) @@ -39,7 +39,7 @@ func RunStoreTests(t *testing.T, store cistore.Store, cleanup func()) { // Clear existing data groups := store.GetGroups() for name := range groups { - store.RemoveGroupData(name) + _ = store.RemoveGroupData(name) // Ignoring error on RemoveGroupData as the test will catch any issues } testClusterDefaultsOperations(t, store) }) diff --git a/pkg/wgtunnel/allocator_test.go b/pkg/wgtunnel/allocator_test.go index 0eb6ba150..4f33353bd 100644 --- a/pkg/wgtunnel/allocator_test.go +++ b/pkg/wgtunnel/allocator_test.go @@ -14,7 +14,7 @@ func TestNewIPAllocator(t *testing.T) { } func TestReserve(t *testing.T) { - allocator, _ := NewIPAllocator("192.168.1.0/24") + allocator, _ := NewIPAllocator("192.168.1.0/24") // ignoring error on NewIPAllocator. We'll catch it on use anyway. ip := net.IPAddr{IP: net.ParseIP("192.168.1.10")} err := allocator.Reserve(ip) if err != nil { @@ -29,7 +29,7 @@ func TestReserve(t *testing.T) { } func TestNextAvailable(t *testing.T) { - allocator, _ := NewIPAllocator("192.168.1.0/24") + allocator, _ := NewIPAllocator("192.168.1.0/24") // ignoring error on NewIPAllocator. We'll catch it on use anyway. // Reserve the first few IPs to test allocation for i := 0; i <= 253; i++ { @@ -55,9 +55,9 @@ func TestNextAvailable(t *testing.T) { } func TestIsAllocated(t *testing.T) { - allocator, _ := NewIPAllocator("192.168.1.0/24") + allocator, _ := NewIPAllocator("192.168.1.0/24") // ignoring error on NewIPAllocator. We'll catch it on use anyway. ip := net.IPAddr{IP: net.ParseIP("192.168.1.10")} - allocator.Reserve(ip) + _ = allocator.Reserve(ip) // ignoring error on Reserve. We'll catch it on use anyway. if !allocator.IsAllocated(ip) { t.Fatalf("Expected IP to be allocated") @@ -72,7 +72,7 @@ func TestIsAllocated(t *testing.T) { func TestRelease(t *testing.T) { allocator, _ := NewIPAllocator("192.168.1.0/24") ip := net.IPAddr{IP: net.ParseIP("192.168.1.10")} - allocator.Reserve(ip) + _ = allocator.Reserve(ip) // ignoring error on Reserve. We'll catch it on use anyway. err := allocator.Release(ip) if err != nil { diff --git a/pkg/wgtunnel/handlers.go b/pkg/wgtunnel/handlers.go index dca87156b..60498477f 100644 --- a/pkg/wgtunnel/handlers.go +++ b/pkg/wgtunnel/handlers.go @@ -45,7 +45,7 @@ type WGResponse struct { // @Failure 500 {object} nil // @Param pubkey body PublicKeyRequest true "WireGuard public key of client" // @Param X-Forwarded-For header string false "Override source IP" -// @Router /cloud-init/wg-init [post] +// @Router /wg-init [post] func AddClientHandler(im *InterfaceManager, smdClient smdclient.SMDClientInterface) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -54,7 +54,9 @@ func AddClientHandler(im *InterfaceManager, smdClient smdclient.SMDClientInterfa } var req PublicKeyRequest - defer r.Body.Close() + defer func() { + _ = r.Body.Close() // ignoring error on deferred Close + }() if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return diff --git a/pkg/wgtunnel/tunnels.go b/pkg/wgtunnel/tunnels.go index eb9bd95d4..1e5aff1b4 100644 --- a/pkg/wgtunnel/tunnels.go +++ b/pkg/wgtunnel/tunnels.go @@ -76,8 +76,10 @@ func NewInterfaceManager(name string, localIp net.IP, network *net.IPNet) *Inter log.Fatal().Err(err).Msg("Failed to get usable IP") } im.ipAddress = net.IPAddr{IP: wgIp, Zone: ""} - im.ipManager.Reserve(im.ipAddress) - + err = im.ipManager.Reserve(im.ipAddress) + if err != nil { + log.Fatal().Err(err).Msg("Failed to reserve IP address") + } return &im } @@ -176,7 +178,7 @@ func (m *InterfaceManager) StartServer() error { // Step 2: Assign IP address to the WireGuard interface wgIp := m.ipAddress.IP.String() - ones, _ := m.network.Mask.Size() + ones, _ := m.network.Mask.Size() // we don't care about the number of bits in the mask wgCidr := fmt.Sprintf("%s/%d", wgIp, ones) if out, err := exec.Command("ip", "address", "add", "dev", m.interfaceName, wgCidr).CombinedOutput(); err != nil { @@ -190,7 +192,9 @@ func (m *InterfaceManager) StartServer() error { if err != nil { return fmt.Errorf("failed to create temporary file for private key: %v", err) } - defer os.Remove(tmpfile.Name()) // Clean up the file afterwards + defer func() { + _ = os.Remove(tmpfile.Name()) // ignoring error on Remove as there's nothing we can do about it anyway + }() if _, err := tmpfile.WriteString(m.privateKey); err != nil { return fmt.Errorf("failed to write private key to temporary file: %v", err)