diff --git a/.github/workflows/build-matrix.json b/.github/workflows/build-matrix.json index f39c7fc7f..d299dd5ce 100644 --- a/.github/workflows/build-matrix.json +++ b/.github/workflows/build-matrix.json @@ -13,6 +13,13 @@ "build-args": "COMPONENT=tenant-operator", "harbor-project": "crownlabs-core" }, + { + "component": "cloudimg-registry", + "context": "./operators", + "dockerfile": "./operators/build/golang-common/Dockerfile", + "build-args": "COMPONENT=cloudimg-registry", + "harbor-project": "crownlabs-core" + }, { "component": "bastion-operator", "context": "./operators", diff --git a/operators/cmd/cloudimg-registry/main.go b/operators/cmd/cloudimg-registry/main.go new file mode 100644 index 000000000..bc8fd4bb0 --- /dev/null +++ b/operators/cmd/cloudimg-registry/main.go @@ -0,0 +1,87 @@ +// Copyright 2020-2024 Politecnico di Torino +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main contains the entrypoint for the cloud image registry. +package main + +import ( + "context" + "flag" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "k8s.io/klog/v2" + + "github.com/netgroup-polito/CrownLabs/operators/pkg/ciregistry" +) + +var ( + dataRoot = flag.String("data-root", "/data", "Root data path for the server") + listenerAddr = flag.String("listener-addr", ":8080", "Address for the server to listen on") + readHeaderTimeoutSeconds = flag.Int("read-header-timeout-secs", 2, "Number of seconds allowed to read request headers") +) + +func main() { + // Initialize klog + klog.InitFlags(nil) + defer klog.Flush() + + // Parse flags + flag.Parse() + + // Update ciregistry configuration + ciregistry.DataRoot = *dataRoot + + // Start the server + server := initializeServer(*listenerAddr, *readHeaderTimeoutSeconds) + + // Graceful shutdown setup + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + + go func() { + klog.Infof("Starting server on %s", server.Addr) + klog.Infof("API documentation available at http://localhost%s/docs", server.Addr) + + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + klog.Fatalf("Server failed: %v", err) + } + }() + + <-stop + klog.Info("Shutting down server gracefully...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + klog.Fatalf("Server forced to shutdown: %v", err) + } + + klog.Info("Server gracefully stopped") +} + +func initializeServer(addr string, readTimeoutSeconds int) *http.Server { + handler := ciregistry.NewRouter() + server := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: time.Duration(readTimeoutSeconds) * time.Second, + } + + return server +} diff --git a/operators/deploy/cloudimg-registry/.helmignore b/operators/deploy/cloudimg-registry/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/operators/deploy/cloudimg-registry/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/operators/deploy/cloudimg-registry/Chart.yaml b/operators/deploy/cloudimg-registry/Chart.yaml new file mode 100644 index 000000000..eff6d895c --- /dev/null +++ b/operators/deploy/cloudimg-registry/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: cloudimg-registry +description: The CrownLabs Cloud Image Registry + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +icon: https://crownlabs.polito.it/images/logo.svg diff --git a/operators/deploy/cloudimg-registry/templates/_helpers.tpl b/operators/deploy/cloudimg-registry/templates/_helpers.tpl new file mode 100644 index 000000000..9474213c4 --- /dev/null +++ b/operators/deploy/cloudimg-registry/templates/_helpers.tpl @@ -0,0 +1,61 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "cloudimg-registry.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If the release name contains the chart name, it will be used as a full name. +*/}} +{{- define "cloudimg-registry.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +The version of the application to be deployed +*/}} +{{- define "cloudimg-registry.version" -}} +{{- if .Values.global }} +{{- .Values.image.tag | default .Values.global.version | default .Chart.AppVersion }} +{{- else }} +{{- .Values.image.tag | default .Chart.AppVersion }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cloudimg-registry.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cloudimg-registry.labels" -}} +helm.sh/chart: {{ include "cloudimg-registry.chart" . }} +{{ include "cloudimg-registry.selectorLabels" . }} +app.kubernetes.io/version: {{ include "cloudimg-registry.version" . | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cloudimg-registry.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cloudimg-registry.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/operators/deploy/cloudimg-registry/templates/deployment.yaml b/operators/deploy/cloudimg-registry/templates/deployment.yaml new file mode 100644 index 000000000..03343aa47 --- /dev/null +++ b/operators/deploy/cloudimg-registry/templates/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cloudimg-registry.fullname" . }} + labels: + {{ include "cloudimg-registry.labels" . | nindent 4 }} +{{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{ include "cloudimg-registry.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "cloudimg-registry.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ include "cloudimg-registry.version" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - "--data-root={{ .Values.configurations.dataRoot }}" + - "--read-header-timeout-secs={{ .Values.configurations.readHeaderTimeoutSeconds }}" + - "--listener-addr=:8080" + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: "{{ include "cloudimg-registry.fullname" . }}-storage" + mountPath: {{ .Values.configurations.dataRoot }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumes: + - name: "{{ include "cloudimg-registry.fullname" . }}-storage" + persistentVolumeClaim: + claimName: "{{ include "cloudimg-registry.fullname" . }}-pvc" diff --git a/operators/deploy/cloudimg-registry/templates/pvc.yaml b/operators/deploy/cloudimg-registry/templates/pvc.yaml new file mode 100644 index 000000000..4963fc2a7 --- /dev/null +++ b/operators/deploy/cloudimg-registry/templates/pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: "{{ include "cloudimg-registry.fullname" . }}-pvc" + labels: + {{ include "cloudimg-registry.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.configurations.volume.accessMode }} + resources: + requests: + storage: {{ .Values.configurations.volume.size }} + storageClassName: {{ .Values.configurations.volume.storageClass }} diff --git a/operators/deploy/cloudimg-registry/templates/service.yaml b/operators/deploy/cloudimg-registry/templates/service.yaml new file mode 100644 index 000000000..168d4aa45 --- /dev/null +++ b/operators/deploy/cloudimg-registry/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cloudimg-registry.fullname" . }} + labels: + {{ include "cloudimg-registry.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{ include "cloudimg-registry.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP \ No newline at end of file diff --git a/operators/deploy/cloudimg-registry/values.yaml b/operators/deploy/cloudimg-registry/values.yaml new file mode 100644 index 000000000..33d78a1d1 --- /dev/null +++ b/operators/deploy/cloudimg-registry/values.yaml @@ -0,0 +1,51 @@ +# Default values for cloudimg registry. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +configurations: + # Comma separated list of whitelisted IPs (can include glob expressions) that can create instances + allowedIPs: "" + targetNamespace: "cloudimg-registry" + dataRoot: "/data" + readHeaderTimeoutSeconds: 2 + volume: + size: "100Gi" + accessMode: "ReadWriteMany" + storageClass: "rook-cephfs-primary" + +image: + repository: crownlabs/cloudimg-registry + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart version. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +deploymentAnnotations: {} +podAnnotations: {} +ingressAnnotations: {} + +podSecurityContext: + fsGroup: 2000 + +securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 2000 + runAsGroup: 2000 + privileged: false + +resources: + limits: + memory: 500Mi + cpu: 1000m + requests: + memory: 500Mi + cpu: 100m diff --git a/operators/go.mod b/operators/go.mod index ae846ee5f..db08c566a 100644 --- a/operators/go.mod +++ b/operators/go.mod @@ -9,12 +9,13 @@ require ( github.com/go-logr/logr v1.4.1 github.com/go-resty/resty/v2 v2.12.0 github.com/golang/mock v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 - github.com/prometheus/client_golang v1.19.0 - golang.org/x/text v0.14.0 + github.com/prometheus/client_golang v1.20.5 + golang.org/x/text v0.16.0 google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 @@ -27,14 +28,17 @@ require ( sigs.k8s.io/controller-runtime v0.17.2 ) +require github.com/vearutop/statigz v1.4.0 // indirect + require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect @@ -50,6 +54,7 @@ require ( github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -57,20 +62,27 @@ require ( github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 // indirect github.com/openshift/custom-resource-status v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect github.com/segmentio/ksuid v1.0.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/swaggest/form/v5 v5.1.1 // indirect + github.com/swaggest/jsonschema-go v0.3.72 // indirect + github.com/swaggest/openapi-go v0.2.54 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/swaggest/rest v0.2.69 // indirect + github.com/swaggest/swgui v1.8.2 + github.com/swaggest/usecase v1.3.1 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/operators/go.sum b/operators/go.sum index ccbeb9a85..4cfb618c4 100644 --- a/operators/go.sum +++ b/operators/go.sum @@ -9,9 +9,10 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -43,6 +44,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -116,6 +119,8 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -128,6 +133,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -137,6 +144,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -180,17 +189,19 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 h1:levPcBfnazlA1CyCMC3asL/QLZkq9pa8tQZOH513zQw= +github.com/santhosh-tekuri/jsonschema/v3 v3.1.0/go.mod h1:8kzK2TC0k0YjOForaAHdNEa7ik0fokNa2k30BKJ/W7Y= github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY= github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -201,14 +212,31 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY= +github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg= +github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNyNzvnVHv4= +github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4= +github.com/swaggest/openapi-go v0.2.54 h1:WnFKIHAgR2RIOiYys3qvSuYmsFd2a17MIoC9Tcvog5c= +github.com/swaggest/openapi-go v0.2.54/go.mod h1:2Q7NpuG9NgpGeTaNOo852GSR6cCzSP4IznA9DNdUTQw= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/swaggest/rest v0.2.69 h1:h0QdL+izv4b3UaBb95USf5xQ6g+6BoanVGDFddBM71w= +github.com/swaggest/rest v0.2.69/go.mod h1:jf/wNhDFY7TPEsSGooy2ZEimtaNEnvpaU6SPlTaWTO4= +github.com/swaggest/swgui v1.8.2 h1:JGpRCLGLZ7EqTwHsBEOo//kx8CM7Rv3RchgvfNpB+6E= +github.com/swaggest/swgui v1.8.2/go.mod h1:nkzGeyMfq5FstGGNJKr1LORvM4RdsjTmvWvqvyZeDDc= +github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w= +github.com/swaggest/usecase v1.3.1/go.mod h1:cae3lDd5VDmM36OQcOOOdAlEDg40TiQYIp99S9ejWqA= +github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU= +github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -227,8 +255,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= @@ -266,11 +295,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -307,26 +337,28 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -345,8 +377,8 @@ golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpd golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -357,8 +389,6 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= @@ -382,8 +412,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/operators/pkg/ciregistry/handlers.go b/operators/pkg/ciregistry/handlers.go new file mode 100644 index 000000000..f1481de74 --- /dev/null +++ b/operators/pkg/ciregistry/handlers.go @@ -0,0 +1,221 @@ +// Copyright 2020-2024 Politecnico di Torino +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ciregistry contains the main server logic +// and http handlers for the CrownLabs cloud image registry +package ciregistry + +import ( + "context" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + "k8s.io/klog/v2" +) + +// HandleGetImages lists all images present in a directory. +func HandleGetImages(log klog.Logger) usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, in URLRepoPath, out *BasicJSONReply) error { + log := log.WithValues("repo", in.Repo) + + if !in.isValid() { + log.Error(nil, "Invalid path parameters") + return status.Wrap(errors.New("invalid path parameters"), status.InvalidArgument) + } + + log.Info("started: Handling GetImages") + + images, err := ListRepoDirs(in.Repo, log.V(1)) + if err != nil { + log.Error(err, "Failed to list repository images") + return err + } + + out.Success = true + out.Data = images + log.Info("success") + + return nil + }) + + u.SetExpectedErrors(status.Internal, status.NotFound, status.InvalidArgument) + + return u +} + +// HandleGetImageTags lists all tags available for an image. +func HandleGetImageTags(log klog.Logger) usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, in URLRepoImagePath, out *BasicJSONReply) error { + log := log.WithValues("repo", in.Repo, "image", in.Image) + + if !in.isValid() { + log.Error(nil, "Invalid path parameters") + return status.Wrap(errors.New("invalid path parameters"), status.InvalidArgument) + } + + log.Info("started: Handling GetImageTags") + + imagePath := filepath.Join(in.Repo, in.Image) + + tags, err := ListRepoDirs(imagePath, log.V(1)) + if err != nil { + log.Error(err, "Failed to list image tags") + return err + } + + out.Success = true + out.Data = tags + log.Info("success") + + return nil + }) + + u.SetExpectedErrors(status.Internal, status.NotFound, status.InvalidArgument) + + return u +} + +// HandleGetImage serves an image file. +func HandleGetImage(log klog.Logger) usecase.Interactor { + return ServeFile("image.bin", "application/octet-stream", log) +} + +// HandleGetImageMeta serves annotations pertaining to a version of an image. +func HandleGetImageMeta(log klog.Logger) usecase.Interactor { + return ServeFile("meta.json", "application/json", log) +} + +// HandlePostImage uploads an image file and related annotations to a directory. +func HandlePostImage(log klog.Logger) usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, in UploadFiles, out *BasicJSONReply) (err error) { + log := log.WithValues("repo", in.Repo, "image", in.Image, "tag", in.Tag) + + if !in.isValid() { + log.Error(nil, "Invalid path parameters") + return status.Wrap(errors.New("invalid path parameters"), status.InvalidArgument) + } + + log.Info("started: Handling PostImage") + + var ( + raw []byte + imgFile *os.File + ) + + imageDir := filepath.Join(DataRoot, in.Repo, in.Image, in.Tag) + err = os.MkdirAll(imageDir, os.ModePerm) + if err != nil { + log.Error(err, "Failed to create directory") + return status.Wrap(err, status.Internal) + } + + defer func() { + clErr := in.MetadataFile.Close() + if clErr != nil && err == nil { + log.Error(clErr, "Failed to close metadata file") + err = clErr + } + + clErr = in.ImageFile.Close() + if clErr != nil && err == nil { + log.Error(clErr, "Failed to close image file") + err = clErr + } + + if err == nil { + out.Success = true + log.Info("success") + } + }() + + raw, err = io.ReadAll(in.MetadataFile) + if err != nil { + log.Error(err, "Failed to read metadata file") + return status.Wrap(err, status.Internal) + } + + var data map[string]string + err = json.Unmarshal(raw, &data) + if err != nil { + log.Error(err, "Failed to parse metadata file") + return status.Wrap(err, status.Internal) + } + raw, err = json.Marshal(data) + if err != nil { + log.Error(err, "Failed to parse metadata file") + return status.Wrap(err, status.Internal) + } + + metaFilePath := filepath.Join(imageDir, "meta.json") + err = os.WriteFile(metaFilePath, raw, 0o600) + if err != nil { + log.Error(err, "Failed to write metadata file") + return status.Wrap(err, status.Internal) + } + + imgFilePath := filepath.Join(imageDir, "image.bin") + safeFilePath := filepath.Clean(imgFilePath) + imgFile, err = os.Create(safeFilePath) + if err != nil { + log.Error(err, "Failed to create image file") + return status.Wrap(err, status.Internal) + } + defer imgFile.Close() + + _, err = io.Copy(imgFile, in.ImageFile) + if err != nil { + log.Error(err, "Failed to copy image file") + return status.Wrap(err, status.Internal) + } + + return err + }) + + u.SetExpectedErrors(status.Internal, status.InvalidArgument) + + return u +} + +// HandleDeleteTag deletes a tag of an image. +func HandleDeleteTag(log klog.Logger) usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, in URLRepoImageTagPath, out *BasicJSONReply) error { + log := log.WithValues("repo", in.Repo, "img", in.Image, "tag", in.Tag) + + if !in.isValid() { + log.Error(nil, "Invalid path parameters") + return status.Wrap(errors.New("invalid path parameters"), status.InvalidArgument) + } + + log.Info("started: Handling DeleteTag") + + err := DeleteImageTag(in.Repo, in.Image, in.Tag, log.V(1)) + if err != nil { + return err + } + + out.Success = true + log.Info("success") + + return nil + }) + + u.SetExpectedErrors(status.Internal, status.NotFound, status.InvalidArgument) + + return u +} diff --git a/operators/pkg/ciregistry/router.go b/operators/pkg/ciregistry/router.go new file mode 100644 index 000000000..778404ea6 --- /dev/null +++ b/operators/pkg/ciregistry/router.go @@ -0,0 +1,50 @@ +// Copyright 2020-2024 Politecnico di Torino +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ciregistry + +import ( + "net/http" + + "github.com/swaggest/openapi-go/openapi3" + "github.com/swaggest/rest/nethttp" + "github.com/swaggest/rest/web" + swgui "github.com/swaggest/swgui/v5cdn" + "k8s.io/klog/v2" +) + +// NewRouter initializes a router for ciregistry service. +func NewRouter() http.Handler { + log := klog.Background() + klog.Info("Initializing router for ciregistry service") + + r := openapi3.NewReflector() + s := web.NewService(r) + + s.OpenAPISchema().SetTitle("Cloud Image Registry") + s.OpenAPISchema().SetDescription("API for managing cloudimage repositories and metadata.") + s.OpenAPISchema().SetVersion("1.0.0") + + s.Get("/{repo}", HandleGetImages(klog.LoggerWithName(log, "imagelist"))) + s.Get("/{repo}/{image}", HandleGetImageTags(klog.LoggerWithName(log, "taglist"))) + s.Get("/{repo}/{image}/{tag}", HandleGetImage(klog.LoggerWithName(log, "imagebin")), nethttp.SuccessfulResponseContentType("application/octet-stream")) + s.Get("/{repo}/{image}/{tag}/meta", HandleGetImageMeta(klog.LoggerWithName(log, "imagemeta"))) + s.Post("/{repo}/{image}/{tag}", HandlePostImage(klog.LoggerWithName(log, "poster")), nethttp.SuccessStatus(http.StatusCreated)) + s.Delete("/{repo}/{image}/{tag}", HandleDeleteTag(klog.LoggerWithName(log, "deleter"))) + + s.Docs("/docs", swgui.New) + + klog.Info("Router initialized successfully") + return s +} diff --git a/operators/pkg/ciregistry/storage.go b/operators/pkg/ciregistry/storage.go new file mode 100644 index 000000000..feff569fc --- /dev/null +++ b/operators/pkg/ciregistry/storage.go @@ -0,0 +1,173 @@ +// Copyright 2020-2024 Politecnico di Torino +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ciregistry + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + + "github.com/go-logr/logr" + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + "k8s.io/klog/v2" +) + +var ( + // DataRoot is the global data root position. + DataRoot = "/data" +) + +// ListRepoDirs lists all directories inside a repository. +func ListRepoDirs(repo string, log logr.Logger) ([]string, error) { + p := filepath.Join(DataRoot, repo) + + if _, err := os.Stat(p); os.IsNotExist(err) || err != nil { + log.Error(err, "Repository directory does not exist") + return nil, status.Wrap(err, status.NotFound) + } + + dirs, err := os.ReadDir(p) + if err != nil { + log.Error(err, "Failed to list directories") + return nil, status.Wrap(err, status.Internal) + } + + var list []string + for _, dir := range dirs { + if dir.IsDir() { + list = append(list, dir.Name()) + } + } + + return list, nil +} + +// DeleteImageTag deletes a tag of an image and +// performs clean-up if necessary. +func DeleteImageTag(repo, image, tag string, log logr.Logger) error { + tagDir := filepath.Join(DataRoot, repo, image, tag) + log.Info("Deleting tag") + + if _, err := os.Stat(tagDir); os.IsNotExist(err) { + log.Error(err, "Tag path does not exist") + return status.Wrap(err, status.NotFound) + } + + err := os.RemoveAll(tagDir) + if err != nil { + log.Error(err, "Failed to delete tag") + return status.Wrap(err, status.Internal) + } + log.Info("Tag deletion completed") + + // Clean up parent directory if empty + imageDir := filepath.Dir(tagDir) + proceed, err := deleteDir(imageDir, log) + if err != nil { + return err + } + + if proceed { + repoDir := filepath.Dir(imageDir) + _, err = deleteDir(repoDir, log) + if err != nil { + return err + } + } + + return nil +} + +// ServeFile serves image.bin and meta.json files. +func ServeFile(fileName, contentType string, log klog.Logger) usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, in URLRepoImageTagPath, out *WriterOutput) error { + log := log.WithValues("repo", in.Repo, "img", in.Image, "tag", in.Tag) + + if !in.isValid() { + log.Error(nil, "Invalid path parameters") + return status.Wrap(errors.New("invalid path parameters"), status.InvalidArgument) + } + + if fileName == "image.bin" { + log.Info("started: Handling GetImage") + } else { + log.Info("started: Handling GetImageMeta") + } + + var ( + err error + file *os.File + ) + + filePath := filepath.Join(DataRoot, in.Repo, in.Image, in.Tag, fileName) + safeFilePath := filepath.Clean(filePath) + + file, err = os.Open(safeFilePath) + if err != nil { + if os.IsNotExist(err) { + log.Error(err, "File not found") + return status.Wrap(errors.New("file not found"), status.NotFound) + } + log.Error(err, "Failed to open file") + return status.Wrap(err, status.Internal) + } + defer func() { + if clErr := file.Close(); clErr != nil { + log.Error(clErr, "Failed to close file") + err = clErr + } + if err == nil { + log.Info("success") + } + }() + + out.ContentType = contentType + _, err = io.Copy(out, file) + if err != nil { + log.Error(err, "Failed to copy file content") + return status.Wrap(err, status.Internal) + } + + return err + }) + + u.SetExpectedErrors(status.NotFound, status.Internal, status.InvalidArgument) + + return u +} + +func deleteDir(dir string, log logr.Logger) (bool, error) { + remaining, err := os.ReadDir(dir) + if err != nil { + log.Error(err, "Failed to read parent directory during cleanup") + return false, status.Wrap(err, status.Internal) + } + + if len(remaining) > 0 { + return false, nil + } + + log.Info("Current directory is empty, starting cleanup") + if err = os.Remove(dir); err != nil { + log.Error(err, "Failed to delete directory") + return false, status.Wrap(err, status.Internal) + } + + log.Info("Directory deletion completed") + return true, nil +} diff --git a/operators/pkg/ciregistry/types.go b/operators/pkg/ciregistry/types.go new file mode 100644 index 000000000..65dde487b --- /dev/null +++ b/operators/pkg/ciregistry/types.go @@ -0,0 +1,80 @@ +// Copyright 2020-2024 Politecnico di Torino +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ciregistry + +import ( + "mime/multipart" + "regexp" + + "github.com/swaggest/usecase" +) + +// URLRepoPath struct for /{repo} endpoints. +type URLRepoPath struct { + Repo string `path:"repo" minLength:"3"` +} + +func (r *URLRepoPath) isValid() bool { + return validatePathSegment(r.Repo) +} + +// URLRepoImagePath struct for /{repo}/{image} endpoints. +type URLRepoImagePath struct { + URLRepoPath + Image string `path:"image" minLength:"3"` +} + +func (r *URLRepoImagePath) isValid() bool { + return r.URLRepoPath.isValid() && validatePathSegment(r.Image) +} + +// URLRepoImageTagPath struct for /{repo}/{image}/{tag} endpoints. +type URLRepoImageTagPath struct { + URLRepoImagePath + Tag string `path:"tag" minLength:"2"` +} + +func (r *URLRepoImageTagPath) isValid() bool { + return r.URLRepoImagePath.isValid() && validatePathSegment(r.Tag) +} + +// UploadFiles struct for uploading image files along with annotations. +type UploadFiles struct { + URLRepoImageTagPath + MetadataFile multipart.File `formData:"annotations"` + ImageFile multipart.File `formData:"img"` +} + +// BasicJSONReply struct of a basic response. +type BasicJSONReply struct { + Success bool `json:"success"` + Data []string `json:"data"` +} + +// WriterOutput struct for serving files. +type WriterOutput struct { + ContentType string `header:"Content-Type" description:"MIME type of the file."` + usecase.OutputWithEmbeddedWriter +} + +// validatePathSegment validates a string against a part of the RFC 1123 Label Names rules: +// - Contains only lowercase alphanumeric characters or '-'. +// - Starts and ends with an alphanumeric character. +// - Length is at least 1 character and at most 63 characters. +// The length requirement is ignored. +func validatePathSegment(segment string) bool { + rfc1123Regex := regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) + return rfc1123Regex.MatchString(segment) +}