From 930efcba46c9522c325a89b19651a973a5042fa3 Mon Sep 17 00:00:00 2001 From: air-31 Date: Fri, 20 Dec 2024 16:55:54 +0100 Subject: [PATCH] Add cloud image registry implementation --- .github/workflows/build-matrix.json | 7 + deploy/crownlabs/Chart.yaml | 4 + deploy/crownlabs/values.yaml | 11 + .../certificate-provisioning/README.md | 3 +- operators/cmd/cloudimg-registry/main.go | 87 +++++++ .../deploy/cloudimg-registry/.helmignore | 23 ++ operators/deploy/cloudimg-registry/Chart.yaml | 20 ++ .../cloudimg-registry/templates/_helpers.tpl | 61 +++++ .../templates/deployment.yaml | 68 +++++ .../cloudimg-registry/templates/pvc.yaml | 13 + .../cloudimg-registry/templates/service.yaml | 15 ++ .../deploy/cloudimg-registry/values.yaml | 47 ++++ operators/go.mod | 35 ++- operators/go.sum | 92 +++++-- operators/pkg/ciregistry/handlers.go | 238 ++++++++++++++++++ operators/pkg/ciregistry/router.go | 62 +++++ operators/pkg/ciregistry/storage.go | 178 +++++++++++++ operators/pkg/ciregistry/types.go | 80 ++++++ 18 files changed, 1004 insertions(+), 40 deletions(-) create mode 100644 operators/cmd/cloudimg-registry/main.go create mode 100644 operators/deploy/cloudimg-registry/.helmignore create mode 100644 operators/deploy/cloudimg-registry/Chart.yaml create mode 100644 operators/deploy/cloudimg-registry/templates/_helpers.tpl create mode 100644 operators/deploy/cloudimg-registry/templates/deployment.yaml create mode 100644 operators/deploy/cloudimg-registry/templates/pvc.yaml create mode 100644 operators/deploy/cloudimg-registry/templates/service.yaml create mode 100644 operators/deploy/cloudimg-registry/values.yaml create mode 100644 operators/pkg/ciregistry/handlers.go create mode 100644 operators/pkg/ciregistry/router.go create mode 100644 operators/pkg/ciregistry/storage.go create mode 100644 operators/pkg/ciregistry/types.go 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/deploy/crownlabs/Chart.yaml b/deploy/crownlabs/Chart.yaml index 8ff8e0cd5..7f46088fd 100644 --- a/deploy/crownlabs/Chart.yaml +++ b/deploy/crownlabs/Chart.yaml @@ -65,6 +65,10 @@ dependencies: repository: file://../../operators/deploy/instmetrics condition: instmetrics.enabled +- name: cloudimg-registry + version: "0.1.0" + repository: file://../../operators/deploy/cloudimg-registry + - name: policies version: "0.1.0" repository: file://../../policies diff --git a/deploy/crownlabs/values.yaml b/deploy/crownlabs/values.yaml index aa2f2c784..c4809154e 100644 --- a/deploy/crownlabs/values.yaml +++ b/deploy/crownlabs/values.yaml @@ -155,6 +155,17 @@ instmetrics: updatePeriod: 4s grpcPort: 9090 +cloudimg-registry: + replicaCount: 1 + configurations: + volume: + size: "100Gi" + accessMode: "ReadWriteMany" + storageClass: "rook-cephfs-primary" + image: + repository: crownlabs/cloudimg-registry + pullPolicy: IfNotPresent + policies: ingressHostnamePattern: s??????.sandbox.crownlabs.polito.it namespaceSelector: diff --git a/infrastructure/certificate-provisioning/README.md b/infrastructure/certificate-provisioning/README.md index fd52e4c03..aed7d0162 100644 --- a/infrastructure/certificate-provisioning/README.md +++ b/infrastructure/certificate-provisioning/README.md @@ -113,12 +113,13 @@ labels: ``` ## Synchronize digital certificates between namespaces +❗❗ `Kubed is no longer available and has been superseded by ConfigSyncer` In different scenarios, it may happen to have different `Ingress` resources in different namespaces which refer to the same domain (with different paths). Unfortunately, annotating all these ingresses with the `cert-manager.io/cluster-issuer` annotation soon leads to hitting the Let's Encrypt rate limits. Hence, it is necessary to introduce some mechanism to synchronize the secret generated between multiple namespaces. One of the projects currently providing a solution to this problem is [kubed](https://github.com/appscode/kubed). ### Install kubed -Kubed can be easily installed with helm [[5]](https://appscode.com/products/kubed/v0.12.0/setup/install/). +Kubed can be easily installed with helm [[5]](https://web.archive.org/web/20230605163413/https://appscode.com/products/kubed/v0.12.0/setup/install/). ```bash helm repo add appscode https://charts.appscode.com/stable/ diff --git a/operators/cmd/cloudimg-registry/main.go b/operators/cmd/cloudimg-registry/main.go new file mode 100644 index 000000000..559fcb499 --- /dev/null +++ b/operators/cmd/cloudimg-registry/main.go @@ -0,0 +1,87 @@ +// Copyright 2020-2025 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..418162d80 --- /dev/null +++ b/operators/deploy/cloudimg-registry/templates/deployment.yaml @@ -0,0 +1,68 @@ +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 }} + initContainers: + - name: fix-permissions + image: busybox:1.36.1 + command: ["sh", "-c", "chown -R {{ .Values.podSecurityContext.fsGroup }}:{{ .Values.podSecurityContext.fsGroup }} {{ .Values.configurations.dataRoot }}"] + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + - name: "{{ include "cloudimg-registry.fullname" . }}-storage" + mountPath: {{ .Values.configurations.dataRoot }} + 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 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: "{{ include "cloudimg-registry.fullname" . }}-storage" + mountPath: {{ .Values.configurations.dataRoot }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + 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..425d6de7e --- /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 diff --git a/operators/deploy/cloudimg-registry/values.yaml b/operators/deploy/cloudimg-registry/values.yaml new file mode 100644 index 000000000..59e3fecbb --- /dev/null +++ b/operators/deploy/cloudimg-registry/values.yaml @@ -0,0 +1,47 @@ +# Default values for cloudimg registry. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +configurations: + 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 + 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..ab152b1d3 100644 --- a/operators/go.mod +++ b/operators/go.mod @@ -11,10 +11,10 @@ require ( github.com/golang/mock v1.6.0 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 @@ -29,12 +29,13 @@ require ( 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 +51,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 +59,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 + github.com/swaggest/openapi-go v0.2.54 + github.com/swaggest/refl v1.3.0 // indirect + github.com/swaggest/rest v0.2.69 + github.com/swaggest/swgui v1.8.2 + github.com/swaggest/usecase v1.3.1 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..e42bdd509 100644 --- a/operators/go.sum +++ b/operators/go.sum @@ -9,9 +9,14 @@ 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/bool64/dev v0.2.36 h1:yU3bbOTujoxhWnt8ig8t94PVmZXIkCaRj9C57OtqJBY= +github.com/bool64/dev v0.2.36/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= 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 +48,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= @@ -118,6 +125,8 @@ github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063/go.mod h1: github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -128,6 +137,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 +148,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,19 +193,23 @@ 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/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -201,14 +218,35 @@ 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/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +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/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 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 +265,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 +305,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 +347,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 +387,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 +399,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 +422,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..8b30996a0 --- /dev/null +++ b/operators/pkg/ciregistry/handlers.go @@ -0,0 +1,238 @@ +// Copyright 2020-2025 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.SetName("GetImages") + 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.SetName("GetImageTags") + 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.SetName("PostImage") + 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.SetName("DeleteTag") + u.SetExpectedErrors(status.Internal, status.NotFound, status.InvalidArgument) + + return u +} + +// HealthzHandler is used for performing readiness probes. +func HealthzHandler() usecase.Interactor { + u := usecase.NewInteractor(func(_ context.Context, _ struct{}, out *BasicJSONReply) error { + out.Success = true + return nil + }) + + u.SetName("ReadinessProbe") + u.SetExpectedErrors(status.Unavailable) + + return u +} diff --git a/operators/pkg/ciregistry/router.go b/operators/pkg/ciregistry/router.go new file mode 100644 index 000000000..a137b4be8 --- /dev/null +++ b/operators/pkg/ciregistry/router.go @@ -0,0 +1,62 @@ +// Copyright 2020-2025 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" + "reflect" + + "github.com/swaggest/jsonschema-go" + "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") + + refl := s.OpenAPIReflector().JSONSchemaReflector() + refl.DefaultOptions = append( + refl.DefaultOptions, + func(rc *jsonschema.ReflectContext) { + rc.DefName = func(t reflect.Type, _ string) string { + return t.Name() + } + }) + + s.Get("/healthz", HealthzHandler()) + 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..33c54f85f --- /dev/null +++ b/operators/pkg/ciregistry/storage.go @@ -0,0 +1,178 @@ +// Copyright 2020-2025 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 + }) + + if fileName == "image.bin" { + u.SetName("GetImage") + } else { + u.SetName("GetImageMeta") + } + 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..ab02b7e66 --- /dev/null +++ b/operators/pkg/ciregistry/types.go @@ -0,0 +1,80 @@ +// Copyright 2020-2025 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) +}