diff --git a/Makefile b/Makefile index b5a9b9a5c5..d301a80249 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,7 @@ OPERATOR_IMG=$(DOCKER_HUB_REPO)/$(DOCKER_HUB_OPERATOR_IMG):$(DOCKER_HUB_OPERATOR OPERATOR_TEST_IMG=$(DOCKER_HUB_REPO)/$(DOCKER_HUB_OPERATOR_TEST_IMG):$(DOCKER_HUB_OPERATOR_TEST_TAG) BUNDLE_IMG=$(DOCKER_HUB_REPO)/$(DOCKER_HUB_BUNDLE_IMG):$(RELEASE_VER) REGISTRY_IMG=$(DOCKER_HUB_REPO)/$(DOCKER_HUB_REGISTRY_IMG):$(RELEASE_VER) + PX_DOC_HOST ?= https://docs.portworx.com PX_INSTALLER_HOST ?= https://install.portworx.com PROMETHEUS_OPERATOR_HELM_CHARTS_TAG ?= kube-prometheus-stack-42.1.0 @@ -78,7 +79,8 @@ BUILD_OPTIONS := -ldflags=$(LDFLAGS) .DEFAULT_GOAL=all .PHONY: operator deploy clean vendor vendor-update test generate manifests tools-check -all: operator pretest downloads +all: operator resource-gateway pretest downloads +dev: operator resource-gateway container deploy vendor-update: go mod download @@ -174,11 +176,16 @@ codegen: @echo "Generating CRD" (GOFLAGS="" hack/update-codegen.sh) -operator: +resource-gateway: + @echo "Building the resource-gateway binary" + @cd cmd/resource-gateway && CGO_ENABLED=0 go build $(BUILD_OPTIONS) -o $(BIN)/resource-gateway + +operator: resource-gateway @echo "Building the cluster operator binary" @cd cmd/operator && CGO_ENABLED=0 go build $(BUILD_OPTIONS) -o $(BIN)/operator @cd cmd/dryrun && CGO_ENABLED=0 go build $(BUILD_OPTIONS) -o $(BIN)/dryrun + container: @echo "Building operator image $(OPERATOR_IMG)" docker build --pull --tag $(OPERATOR_IMG) -f build/Dockerfile . @@ -298,3 +305,6 @@ clean: clean-release-manifest clean-bundle @go clean -i $(PKGS) @echo "Deleting image "$(OPERATOR_IMG) @docker rmi -f $(OPERATOR_IMG) registry.access.redhat.com/ubi9-minimal:latest + +resource-gateway-proto: + $(MAKE) -C proto docker-proto \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile index f9e5cd7272..721276584a 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -22,3 +22,4 @@ COPY manifests /manifests COPY bin/configs /configs COPY bin/operator / COPY bin/dryrun / +COPY bin/resource-gateway / diff --git a/cmd/resource-gateway/resource_gateway.go b/cmd/resource-gateway/resource_gateway.go new file mode 100644 index 0000000000..ab296845bf --- /dev/null +++ b/cmd/resource-gateway/resource_gateway.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "log" + _ "net/http/pprof" + "os" + "strings" + + "github.com/libopenstorage/grpc-framework/pkg/auth" + grpcFramework "github.com/libopenstorage/grpc-framework/server" + + pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" + resourceGateway "github.com/libopenstorage/operator/pkg/resource-gateway" + "github.com/libopenstorage/operator/pkg/version" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "resource-gateway" + app.Usage = "gRPC service for managing resources" + app.Version = version.Version + app.Action = run + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "serverHost", + Usage: "Host for resource-gateway gRPC server", + }, + cli.StringFlag{ + Name: "serverPort", + Usage: "Port for resource-gateway gRPC server", + }, + cli.StringFlag{ + Name: "namespace", + Usage: "Name of the configmap to use for semaphore", + }, + cli.StringFlag{ + Name: "configMapName", + Usage: "Name of the configmap to use for semaphore", + }, + cli.StringFlag{ + Name: "configMapLabels", + Usage: "Labels to use for the configmap", + }, + cli.DurationFlag{ + Name: "configMapUpdatePeriod", + Usage: "Time period between configmap updates", + }, + cli.DurationFlag{ + Name: "deadClientTimeout", + Usage: "Time period after which a node is considered dead", + }, + cli.BoolFlag{ + Name: "debug", + Usage: "Set log level to debug", + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalf("Error starting resource gateway gRPC server: %v", err) + } +} + +// run is the main function for resource-gateway gRPC server +// it initializes the k8s client, creates the gRPC server, and runs the server... +func run(c *cli.Context) { + if c.Bool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + + resourceGatewayServer := resourceGateway.NewResourceGatewayServer( + newResourceGatewayServerConfig(c), + newSemaphoreConfig(c)) + err := resourceGatewayServer.SetupSigIntHandler() + if err != nil { + logrus.Fatalf("Failed to setup signal handler: %v", err) + } + err = resourceGatewayServer.Start() + if err != nil { + logrus.Fatalf("Failed to start resource-gateway server: %v", err) + } +} + +// newResourceGatewayServerConfig creates the config for resource-gateway gRPC server +func newResourceGatewayServerConfig(c *cli.Context) *grpcFramework.ServerConfig { + resourceGatewayServerConfig := resourceGateway.NewResourceGatewayServerConfig() + + serverName := c.String("serverName") + if serverName == "" { + resourceGatewayServerConfig.Name = serverName + } + + serverHost := c.String("serverHost") + serverPort := c.String("serverPort") + if serverHost != "" && serverPort != "" { + serverAddress := fmt.Sprintf("%s:%s", serverHost, serverPort) + resourceGatewayServerConfig.Address = serverAddress + } + + // if Px security is enabled, then Issuer and SharedSecret will be set in the environment + authIssuer := os.Getenv(pxutil.EnvKeyPortworxAuthJwtIssuer) + authSharedSecret := os.Getenv(pxutil.EnvKeyPortworxAuthJwtSharedSecret) + if authIssuer != "" && authSharedSecret != "" { + security := &grpcFramework.SecurityConfig{} + authenticator, err := auth.NewJwtAuthenticator( + &auth.JwtAuthConfig{ + SharedSecret: []byte(authSharedSecret), + }) + if err != nil { + log.Fatalf("unable to create shared key authenticator") + } + security.Authenticators = map[string]auth.Authenticator{ + authIssuer: authenticator, + } + resourceGatewayServerConfig.Security = security + } + + return resourceGatewayServerConfig +} + +// newSemaphoreConfig creates a SemaphoreConfig object with provided +// cli arguments to initialize a new semaphore server +func newSemaphoreConfig(c *cli.Context) *resourceGateway.SemaphoreConfig { + semaphoreConfig := resourceGateway.NewSemaphoreConfig() + if c.String("configMapName") != "" { + semaphoreConfig.ConfigMapName = c.String("configMapName") + } + if c.String("namespace") != "" { + semaphoreConfig.ConfigMapNamespace = c.String("namespace") + } + if c.String("configMapLabels") != "" { + configMapLabels := make(map[string]string) + for _, kv := range strings.Split(c.String("configMapLabels"), ",") { + kvSplit := strings.Split(kv, "=") + if len(kvSplit) != 2 { + logrus.Errorf("Invalid configMapLabels: %s", kvSplit) + continue + } + configMapLabels[kvSplit[0]] = kvSplit[1] + } + semaphoreConfig.ConfigMapLabels = configMapLabels + } + if c.Duration("configMapUpdatePeriod") != 0 { + semaphoreConfig.ConfigMapUpdatePeriod = c.Duration("configMapUpdatePeriod") + } + if c.Duration("deadClientTimeout") != 0 { + semaphoreConfig.DeadClientTimeout = c.Duration("deadClientTimeout") + } + return semaphoreConfig +} diff --git a/deploy/crds/core_v1_storagecluster_crd.yaml b/deploy/crds/core_v1_storagecluster_crd.yaml index 021be0af90..4009485109 100644 --- a/deploy/crds/core_v1_storagecluster_crd.yaml +++ b/deploy/crds/core_v1_storagecluster_crd.yaml @@ -931,6 +931,45 @@ spec: cpu: type: string description: CPU limit. + resourceGateway: + type: object + description: Contains spec of resource-gateway component for storage driver. + properties: + enabled: + type: boolean + description: Flag indicating whether resource-gateway needs to be enabled. + image: + type: string + description: Docker image of the resource-gateway container. + args: + type: object + x-kubernetes-preserve-unknown-fields: true + description: >- + It is a map of arguments provided to resource-gateway. Example: log-level: debug + resources: + type: object + description: Specifies the resource requirements for the resource-gateway pod. + properties: + requests: + type: object + description: Requested resources. + properties: + memory: + type: string + description: Requested memory. + cpu: + type: string + description: Requested cpu. + limits: + type: object + description: Resource limit. + properties: + memory: + type: string + description: Memory limit. + cpu: + type: string + description: CPU limit. monitoring: type: object description: Contains monitoring configuration for the storage cluster. @@ -3902,6 +3941,9 @@ spec: dynamicPluginProxy: type: string description: Desired image for nginx proxy image. + resourceGateway: + type: string + description: Desired image for px resource gateway. conditions: type: array description: Contains details for the current condition of this cluster. diff --git a/drivers/storage/portworx/component/component.go b/drivers/storage/portworx/component/component.go index a5fe79dd6d..7b80b972cb 100644 --- a/drivers/storage/portworx/component/component.go +++ b/drivers/storage/portworx/component/component.go @@ -59,6 +59,7 @@ func Register(name string, c PortworxComponent) { registerLock.Lock() defer registerLock.Unlock() components[name] = c + } // Get returns a PortworxComponent if present else returns (nil, false) diff --git a/drivers/storage/portworx/component/resource_gateway.go b/drivers/storage/portworx/component/resource_gateway.go new file mode 100644 index 0000000000..5624130976 --- /dev/null +++ b/drivers/storage/portworx/component/resource_gateway.go @@ -0,0 +1,421 @@ +package component + +import ( + "context" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/go-version" + pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" + corev1 "github.com/libopenstorage/operator/pkg/apis/core/v1" + "github.com/libopenstorage/operator/pkg/util" + k8sutil "github.com/libopenstorage/operator/pkg/util/k8s" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // ResourceGatewayComponentName name of the ResourceGateway component + ResourceGatewayComponentName = "ResourceGateway" + // resourceGatewayStr common string value for resourceGateway k8s subparts + resourceGatewayStr = "resource-gateway" + + // names for resource-gateway kubernetes resources + // + // ResourceGatewayServiceAccountName name of the resourceGateway service account + ResourceGatewayServiceAccountName = resourceGatewayStr + // ResourceGatewayRoleName name of the resourceGateway cluster role + ResourceGatewayRoleName = resourceGatewayStr + // ResourceGatewayRoleBindingName name of the resourceGateway cluster role binding + ResourceGatewayRoleBindingName = resourceGatewayStr + // ResourceGatewayDeploymentName name of the resourceGateway deployment + ResourceGatewayDeploymentName = resourceGatewayStr + // ResourceGatewayServiceName name of the resourceGateway service + ResourceGatewayServiceName = resourceGatewayStr + // ResourceGatewayContainerName name of the resourceGateway container + ResourceGatewayContainerName = resourceGatewayStr + // ResourceGatewayLabelName label name for resourceGateway + ResourceGatewayLabelName = resourceGatewayStr + + // configuration values for resource-gateway deployment and service + // + // resourceGatewayPortName name of the resourceGateway port + resourceGatewayPortName = "resource-gate" // must be no more than 15 characters + // resourceGatewayDeploymentHost is the host resourceGateway deployment listens on + resourceGatewayDeploymentHost = "0.0.0.0" + // resourceGatewayPort common port for resourceGateway components + resourceGatewayPort = 50051 + // resourceGatewayDeploymentPort is the port resourceGateway deployment listens on + resourceGatewayDeploymentPort = resourceGatewayPort + // resourceGatewayServicePort is the port resourceGateway service listens on + resourceGatewayServicePort = resourceGatewayPort + + // configuration for resource-gateway deployment liveliness probe + // + // resourceGatewayLivenessProbeInitialDelaySeconds is the initial delay for the resource-gateway liveness probe + resourceGatewayLivenessProbeInitialDelaySeconds = 10 + // resourceGatewayLivenessProbePeriodSeconds is the period for the resource-gateway liveness probe + resourceGatewayLivenessProbePeriodSeconds = 2 +) + +var defaultResourceGatewayCommandArgs = map[string]string{ + "serverHost": resourceGatewayDeploymentHost, + "serverPort": strconv.Itoa(resourceGatewayPort), +} + +// resourceGateway is the PortworxComponent implementation for the resource-gateway component +type resourceGateway struct { + k8sClient client.Client +} + +func (r *resourceGateway) Name() string { + return ResourceGatewayComponentName +} + +func (r *resourceGateway) Priority() int32 { + return DefaultComponentPriority +} + +func (r *resourceGateway) Initialize( + k8sClient client.Client, + _ version.Version, + _ *runtime.Scheme, + _ record.EventRecorder, +) { + r.k8sClient = k8sClient +} + +func (r *resourceGateway) IsPausedForMigration(cluster *corev1.StorageCluster) bool { + return util.ComponentsPausedForMigration(cluster) +} + +func (r *resourceGateway) IsEnabled(cluster *corev1.StorageCluster) bool { + return cluster.Spec.ResourceGateway != nil && cluster.Spec.ResourceGateway.Enabled +} + +func (r *resourceGateway) Reconcile(cluster *corev1.StorageCluster) error { + ownerRef := metav1.NewControllerRef(cluster, pxutil.StorageClusterKind()) + if err := r.createServiceAccount(cluster.Namespace, ownerRef); err != nil { + return err + } + if err := r.createRole(cluster.Namespace, ownerRef); err != nil { + return err + } + if err := r.createRoleBinding(cluster.Namespace, ownerRef); err != nil { + return err + } + if err := r.createDeployment(cluster, ownerRef); err != nil { + return err + } + if err := r.createService(cluster, ownerRef); err != nil { + return err + } + return nil +} + +func (r *resourceGateway) Delete(cluster *corev1.StorageCluster) error { + ownerRef := metav1.NewControllerRef(cluster, pxutil.StorageClusterKind()) + if err := k8sutil.DeleteServiceAccount(r.k8sClient, ResourceGatewayServiceAccountName, cluster.Namespace, *ownerRef); err != nil { + return err + } + if err := k8sutil.DeleteRole(r.k8sClient, ResourceGatewayRoleName, cluster.Namespace, *ownerRef); err != nil { + return err + } + if err := k8sutil.DeleteRoleBinding(r.k8sClient, ResourceGatewayRoleBindingName, cluster.Namespace, *ownerRef); err != nil { + return err + } + if err := k8sutil.DeleteDeployment(r.k8sClient, ResourceGatewayDeploymentName, cluster.Namespace, *ownerRef); err != nil { + return err + } + if err := k8sutil.DeleteService(r.k8sClient, ResourceGatewayServiceName, cluster.Namespace, *ownerRef); err != nil { + return err + } + return nil +} + +func (r *resourceGateway) MarkDeleted() {} + +func (r *resourceGateway) createServiceAccount( + clusterNamespace string, + ownerRef *metav1.OwnerReference, +) error { + return k8sutil.CreateOrUpdateServiceAccount( + r.k8sClient, + &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: ResourceGatewayServiceAccountName, + Namespace: clusterNamespace, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + }, + }, + ownerRef, + ) +} + +func (r *resourceGateway) createRole( + clusterNamespace string, + ownerRef *metav1.OwnerReference, +) error { + return k8sutil.CreateOrUpdateRole( + r.k8sClient, + &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: ResourceGatewayRoleName, + Namespace: clusterNamespace, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"create", "get", "list", "patch", "update", "watch"}, + }, + }, + }, + ownerRef, + ) +} + +func (r *resourceGateway) createRoleBinding( + clusterNamespace string, + ownerRef *metav1.OwnerReference, +) error { + return k8sutil.CreateOrUpdateRoleBinding( + r.k8sClient, + &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: ResourceGatewayRoleBindingName, + Namespace: clusterNamespace, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ResourceGatewayServiceAccountName, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: ResourceGatewayRoleName, + APIGroup: "rbac.authorization.k8s.io", + }, + }, + ownerRef, + ) +} + +func (r *resourceGateway) getImage(cluster *corev1.StorageCluster) string { + image := cluster.Spec.ResourceGateway.Image + if image == "" { + image = cluster.Status.DesiredImages.ResourceGateway + } + if image != "" { + image = util.GetImageURN(cluster, image) + } + return image +} + +// getCommand returns the command to run the resource-gateway server with custom configuration +// it uses the configuration values from the StorageCluster spec and sets default values if not present +func (r *resourceGateway) getCommand(cluster *corev1.StorageCluster) []string { + args := map[string]string{} + + // parse user arguments from the StorageCluster spec + for k, v := range cluster.Spec.ResourceGateway.Args { + key := strings.TrimLeft(k, "-") + if len(key) > 0 && len(v) > 0 { + args[key] = v + } + } + + // fill in the missing arguments with default values + defaultResourceGatewayCommandArgs["namespace"] = cluster.Namespace // set namespace + for k, v := range defaultResourceGatewayCommandArgs { + if _, ok := args[k]; !ok { + args[k] = v + } + } + + argList := make([]string, 0) + for k, v := range args { + argList = append(argList, fmt.Sprintf("--%s=%s", k, v)) + } + sort.Strings(argList) + + command := append([]string{"/resource-gateway"}, argList...) + return command +} + +func (r *resourceGateway) createDeployment( + cluster *corev1.StorageCluster, + ownerRef *metav1.OwnerReference, +) error { + // get the existing deployment + existingDeployment := &appsv1.Deployment{} + err := r.k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Name: ResourceGatewayDeploymentName, + Namespace: cluster.Namespace, + }, + existingDeployment, + ) + if err != nil && !errors.IsNotFound(err) { + return err + } + + // get the target deployment + imageName := r.getImage(cluster) + if imageName == "" { + return fmt.Errorf("px reosurce gateway image cannot be empty") + } + command := r.getCommand(cluster) + targetDeployment := r.getResourceGatewayDeploymentSpec(cluster, ownerRef, imageName, command) + + // compare existing and target deployments and create/update the deployment if necessary + isPodTemplateEqual, _ := util.DeepEqualPodTemplate(&targetDeployment.Spec.Template, &existingDeployment.Spec.Template) + if !isPodTemplateEqual { + if err = k8sutil.CreateOrUpdateDeployment(r.k8sClient, targetDeployment, ownerRef); err != nil { + return err + } + } + return nil +} + +func (r *resourceGateway) createService( + cluster *corev1.StorageCluster, + ownerRef *metav1.OwnerReference, +) error { + return k8sutil.CreateOrUpdateService( + r.k8sClient, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ResourceGatewayServiceName, + Namespace: cluster.Namespace, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + Selector: map[string]string{ + "name": ResourceGatewayLabelName, + }, + Ports: []v1.ServicePort{ + { + Name: resourceGatewayPortName, + Port: resourceGatewayServicePort, + TargetPort: intstr.FromInt(resourceGatewayDeploymentPort), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + ownerRef, + ) +} + +func (r *resourceGateway) getResourceGatewayDeploymentSpec( + cluster *corev1.StorageCluster, + ownerRef *metav1.OwnerReference, + imageName string, + command []string, +) *appsv1.Deployment { + commonLabels := map[string]string{ + "name": ResourceGatewayLabelName, + "tier": "control-plane", + } + + replicas := int32(1) + imagePullPolicy := pxutil.ImagePullPolicy(cluster) + + // get the security environment variables + envMap := map[string]*v1.EnvVar{} + pxutil.PopulateSecurityEnvironmentVariables(cluster, envMap) + envList := make([]v1.EnvVar, 0) + for _, env := range envMap { + envList = append(envList, *env) + } + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: ResourceGatewayDeploymentName, + Namespace: cluster.Namespace, + Labels: commonLabels, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: commonLabels, + }, + Replicas: &replicas, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: commonLabels, + }, + Spec: v1.PodSpec{ + ServiceAccountName: ResourceGatewayServiceAccountName, + Containers: []v1.Container{ + { + Name: ResourceGatewayContainerName, + Image: imageName, + ImagePullPolicy: imagePullPolicy, + Env: envList, + Command: command, + SecurityContext: &v1.SecurityContext{ + AllowPrivilegeEscalation: boolPtr(false), + Privileged: boolPtr(false), + }, + Ports: []v1.ContainerPort{ + { + Name: resourceGatewayPortName, + ContainerPort: resourceGatewayDeploymentPort, + Protocol: v1.ProtocolTCP, + }, + }, + LivenessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + GRPC: &v1.GRPCAction{ + Port: resourceGatewayServicePort, + }, + }, + InitialDelaySeconds: resourceGatewayLivenessProbeInitialDelaySeconds, + PeriodSeconds: resourceGatewayLivenessProbePeriodSeconds, + }, + }, + }, + }, + }, + }, + } + + if cluster.Spec.ImagePullSecret != nil && *cluster.Spec.ImagePullSecret != "" { + deployment.Spec.Template.Spec.ImagePullSecrets = append( + []v1.LocalObjectReference{}, + v1.LocalObjectReference{ + Name: *cluster.Spec.ImagePullSecret, + }, + ) + } + + if cluster.Spec.ResourceGateway.Resources != nil { + deployment.Spec.Template.Spec.Containers[0].Resources = *cluster.Spec.ResourceGateway.Resources + } + + return deployment +} + +// RegisterResourceGatewayComponent registers the ResourceGateway component +func RegisterResourceGatewayComponent() { + Register(ResourceGatewayComponentName, &resourceGateway{}) +} + +func init() { + RegisterResourceGatewayComponent() +} diff --git a/drivers/storage/portworx/components_test.go b/drivers/storage/portworx/components_test.go index 6a2de2ced1..766f74c4b3 100644 --- a/drivers/storage/portworx/components_test.go +++ b/drivers/storage/portworx/components_test.go @@ -3345,6 +3345,221 @@ func TestLighthouseSidecarsOverrideWithEnv(t *testing.T) { require.Equal(t, "docker.io/test/stork-connector:t2", image) } +func setupTestEnv(t *testing.T) (portworx, client.Client) { + // Start test with newer version (1.25 beyond) of Kubernetes first, on which PodSecurityPolicy is no longer existing + mockCtrl := gomock.NewController(t) + versionClient := fakek8sclient.NewSimpleClientset() + versionClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ + GitVersion: "v1.25.0", + } + setUpMockCoreOps(mockCtrl, versionClient) + fakeExtClient := fakeextclient.NewSimpleClientset() + apiextensionsops.SetInstance(apiextensionsops.New(fakeExtClient)) + reregisterComponents() + k8sClient := testutil.FakeK8sClient() + driver := portworx{} + err := driver.Init(k8sClient, runtime.NewScheme(), record.NewFakeRecorder(0)) + require.NoError(t, err) + return driver, k8sClient +} + +func createStorageClusterWithResourceGateway() *corev1.StorageCluster { + return &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "px-cluster", + Namespace: "kube-test", + }, + Spec: corev1.StorageClusterSpec{ + Monitoring: &corev1.MonitoringSpec{Telemetry: &corev1.TelemetrySpec{}}, // required to run test + ResourceGateway: &corev1.ResourceGatewaySpec{ + Enabled: true, + Image: "portworx/operator:1.1.1", + }, + }, + } +} + +func TestResourceGatewayBasicInstall(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + driver, k8sClient := setupTestEnv(t) + + // create StorageCluster with ResourceGateway enabled + cluster := createStorageClusterWithResourceGateway() + + err := driver.SetDefaultsOnStorageCluster(cluster) + require.NoError(t, err) + + err = driver.PreInstall(cluster) + require.NoError(t, err) + + // Resource Gateway ServiceAccount + serviceAccount := &v1.ServiceAccount{} + err = testutil.Get(k8sClient, serviceAccount, component.ResourceGatewayServiceAccountName, cluster.Namespace) + require.NoError(t, err) + require.Equal(t, component.ResourceGatewayServiceAccountName, serviceAccount.Name) + require.Equal(t, cluster.Namespace, serviceAccount.Namespace) + require.Len(t, serviceAccount.OwnerReferences, 1) + require.Equal(t, cluster.Name, serviceAccount.OwnerReferences[0].Name) + + // Resource Gateway Role + expectedRole := testutil.GetExpectedRole(t, "resourceGatewayRole.yaml") + actualRole := &rbacv1.Role{} + err = testutil.Get(k8sClient, actualRole, component.ResourceGatewayRoleName, cluster.Namespace) + require.NoError(t, err) + require.Equal(t, expectedRole.Name, actualRole.Name) + require.Empty(t, actualRole.OwnerReferences) + require.ElementsMatch(t, expectedRole.Rules, actualRole.Rules) + + // Resource Gateway RoleBinding + expectedRoleBinding := testutil.GetExpectedRoleBinding(t, "resourceGatewayRoleBinding.yaml") + actualRoleBinding := &rbacv1.RoleBinding{} + err = testutil.Get(k8sClient, actualRoleBinding, component.ResourceGatewayRoleBindingName, cluster.Namespace) + require.NoError(t, err) + require.Equal(t, expectedRoleBinding.Name, actualRoleBinding.Name) + require.Empty(t, actualRoleBinding.OwnerReferences) + require.ElementsMatch(t, expectedRoleBinding.Subjects, actualRoleBinding.Subjects) + require.Equal(t, expectedRoleBinding.RoleRef, actualRoleBinding.RoleRef) + + // Resource Gateway Deployment + expectedDeployment := testutil.GetExpectedDeployment(t, "resourceGatewayDeployment.yaml") + resourceGatewayDeployment := &appsv1.Deployment{} + err = testutil.Get(k8sClient, resourceGatewayDeployment, component.ResourceGatewayDeploymentName, cluster.Namespace) + require.NoError(t, err) + resourceGatewayDeployment.ResourceVersion = "" + require.Equal(t, expectedDeployment, resourceGatewayDeployment) + + // Resource Gateway Service + expectedService := testutil.GetExpectedService(t, "resourceGatewayService.yaml") + actualService := &v1.Service{} + err = testutil.Get(k8sClient, actualService, component.ResourceGatewayServiceName, cluster.Namespace) + require.NoError(t, err) + require.Equal(t, expectedService.Name, actualService.Name) + require.Equal(t, expectedService.Namespace, actualService.Namespace) + require.Len(t, actualService.OwnerReferences, 1) + require.Equal(t, cluster.Name, actualService.OwnerReferences[0].Name) + require.Equal(t, expectedService.Labels, actualService.Labels) + require.Equal(t, expectedService.Spec, actualService.Spec) +} + +func TestResourceGatewayWithSecurity(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + driver, k8sClient := setupTestEnv(t) + + // create StorageCluster with ResourceGateway and Security enabled + cluster := createStorageClusterWithResourceGateway() + cluster.Spec.Security = &corev1.SecuritySpec{ + Enabled: true, + Auth: &corev1.AuthSpec{ + SelfSigned: &corev1.SelfSignedSpec{ + Issuer: stringPtr(defaultSelfSignedIssuer), + TokenLifetime: stringPtr(defaultTokenLifetime), + SharedSecret: stringPtr(pxutil.SecurityPXSharedSecretSecretName), + }, + }, + } + + err := driver.SetDefaultsOnStorageCluster(cluster) + require.NoError(t, err) + + err = driver.PreInstall(cluster) + require.NoError(t, err) + + // Resource Gateway Deployment + resourceGatewayDeployment := &appsv1.Deployment{} + err = testutil.Get(k8sClient, resourceGatewayDeployment, component.ResourceGatewayDeploymentName, cluster.Namespace) + require.NoError(t, err) + + jwtIssuer := "" + var sharedSecretSet, appsSecretSet, systemSecretSet bool + actualContainer := resourceGatewayDeployment.Spec.Template.Spec.Containers[0] + + for _, env := range actualContainer.Env { + if env.Name == pxutil.EnvKeyPortworxAuthJwtIssuer { + jwtIssuer = env.Value + } + if env.Name == pxutil.EnvKeyPortworxAuthJwtSharedSecret { + sharedSecretSet = true + } + if env.Name == pxutil.EnvKeyPortworxAuthSystemKey { + systemSecretSet = true + } + if env.Name == pxutil.EnvKeyPortworxAuthSystemAppsKey { + appsSecretSet = true + } + } + assert.Equal(t, defaultSelfSignedIssuer, jwtIssuer) + assert.True(t, sharedSecretSet) + assert.True(t, systemSecretSet) + assert.True(t, appsSecretSet) +} + +func TestResourceGatewayWithCustomArguments(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + driver, k8sClient := setupTestEnv(t) + + // create StorageCluster with ResourceGateway with custom arguments provided + cluster := createStorageClusterWithResourceGateway() + cluster.Spec.ResourceGateway.Args = map[string]string{ + "configMapUpdatePeriod": "10s", + "deadClientTimeout": "30s", + } + + err := driver.SetDefaultsOnStorageCluster(cluster) + require.NoError(t, err) + + err = driver.PreInstall(cluster) + require.NoError(t, err) + + // Resource Gateway Deployment + resourceGatewayDeployment := &appsv1.Deployment{} + err = testutil.Get(k8sClient, resourceGatewayDeployment, component.ResourceGatewayDeploymentName, cluster.Namespace) + require.NoError(t, err) + + expectedCommand := []string{ + "/resource-gateway", + "--configMapUpdatePeriod=10s", + "--deadClientTimeout=30s", + "--namespace=kube-test", + "--serverHost=0.0.0.0", + "--serverPort=50051", + } + + actualCommand := resourceGatewayDeployment.Spec.Template.Spec.Containers[0].Command + require.Equal(t, expectedCommand, actualCommand) +} + +func TestResourceGatewayWithCustomResources(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + driver, k8sClient := setupTestEnv(t) + + // create StorageCluster with ResourceGateway with custom resources limits and requests + cluster := createStorageClusterWithResourceGateway() + cluster.Spec.ResourceGateway.Resources = &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("2"), + v1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + } + + err := driver.SetDefaultsOnStorageCluster(cluster) + require.NoError(t, err) + + err = driver.PreInstall(cluster) + require.NoError(t, err) + + // Resource Gateway Deployment + resourceGatewayDeployment := &appsv1.Deployment{} + err = testutil.Get(k8sClient, resourceGatewayDeployment, component.ResourceGatewayDeploymentName, cluster.Namespace) + require.NoError(t, err) + + require.Equal(t, *cluster.Spec.ResourceGateway.Resources, resourceGatewayDeployment.Spec.Template.Spec.Containers[0].Resources) + +} + func TestAutopilotInstall(t *testing.T) { // Start test with newer version (1.25 beyond) of Kubernetes first, on which PodSecurityPolicy is no longer existing mockCtrl := gomock.NewController(t) @@ -3496,7 +3711,6 @@ func TestAutopilotInstall(t *testing.T) { require.Equal(t, expectedCR.Name, actualCR.Name) require.Empty(t, actualCR.OwnerReferences) require.ElementsMatch(t, expectedCR.Rules, actualCR.Rules) - } func TestAutopilotWithoutImage(t *testing.T) { @@ -17816,6 +18030,7 @@ func reregisterComponents() { component.RegisterTelemetryComponent() component.RegisterSCCComponent() component.RegisterPortworxPluginComponent() + component.RegisterResourceGatewayComponent() pxutil.SpecsBaseDir = func() string { return "../../../bin/configs" } diff --git a/drivers/storage/portworx/testspec/prometheusRule.yaml b/drivers/storage/portworx/testspec/prometheusRule.yaml index b956758cb2..24a1cd1b2e 100644 --- a/drivers/storage/portworx/testspec/prometheusRule.yaml +++ b/drivers/storage/portworx/testspec/prometheusRule.yaml @@ -205,7 +205,7 @@ spec: expr: px_alerts_poolexpandsuccessful > 1 labels: issue: Portworx pool expand successful. - severity: warning + severity: info resource_type: portworx-node resource_name: "{{$labels.node}}" scrape_target_type: portworx-node @@ -231,7 +231,7 @@ spec: expr: px_alerts_volumeresizesuccessful == 1 labels: issue: Portworx volume resize successful. - severity: warning + severity: info resource_type: portworx-volume resource_name: "{{$labels.volumeid}}" scrape_target_type: portworx-node diff --git a/drivers/storage/portworx/testspec/resourceGatewayDeployment.yaml b/drivers/storage/portworx/testspec/resourceGatewayDeployment.yaml new file mode 100644 index 0000000000..599fed2e08 --- /dev/null +++ b/drivers/storage/portworx/testspec/resourceGatewayDeployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + name: resource-gateway + tier: control-plane + name: resource-gateway + namespace: kube-test + ownerReferences: + - apiVersion: core.libopenstorage.org/v1 + blockOwnerDeletion: true + controller: true + kind: StorageCluster + name: px-cluster +spec: + selector: + matchLabels: + name: resource-gateway + tier: control-plane + replicas: 1 + template: + metadata: + labels: + name: resource-gateway + tier: control-plane + spec: + serviceAccountName: resource-gateway + containers: + - name: resource-gateway + image: docker.io/portworx/operator:1.1.1 + imagePullPolicy: Always + command: + - /resource-gateway + - --namespace=kube-test + - --serverHost=0.0.0.0 + - --serverPort=50051 + securityContext: + allowPrivilegeEscalation: false + privileged: false + livenessProbe: + grpc: + port: 50051 + initialDelaySeconds: 10 + periodSeconds: 2 + ports: + - containerPort: 50051 + name: resource-gate + protocol: TCP \ No newline at end of file diff --git a/drivers/storage/portworx/testspec/resourceGatewayRole.yaml b/drivers/storage/portworx/testspec/resourceGatewayRole.yaml new file mode 100644 index 0000000000..48b9ea883c --- /dev/null +++ b/drivers/storage/portworx/testspec/resourceGatewayRole.yaml @@ -0,0 +1,9 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: resource-gateway + namespace: kube-test +rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "get", "list", "patch", "update", "watch"] diff --git a/drivers/storage/portworx/testspec/resourceGatewayRoleBinding.yaml b/drivers/storage/portworx/testspec/resourceGatewayRoleBinding.yaml new file mode 100644 index 0000000000..3b81e8dfec --- /dev/null +++ b/drivers/storage/portworx/testspec/resourceGatewayRoleBinding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: resource-gateway + namespace: kube-test +subjects: +- kind: ServiceAccount + name: resource-gateway +roleRef: + kind: Role + name: resource-gateway + apiGroup: rbac.authorization.k8s.io diff --git a/drivers/storage/portworx/testspec/resourceGatewayService.yaml b/drivers/storage/portworx/testspec/resourceGatewayService.yaml new file mode 100644 index 0000000000..e4003f0933 --- /dev/null +++ b/drivers/storage/portworx/testspec/resourceGatewayService.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: resource-gateway + namespace: kube-test +spec: + type: ClusterIP + ports: + - name: resource-gate + port: 50051 + targetPort: 50051 + protocol: TCP + selector: + name: resource-gateway diff --git a/drivers/storage/portworx/util/util.go b/drivers/storage/portworx/util/util.go index 15cf2c9c09..c69e2f69aa 100644 --- a/drivers/storage/portworx/util/util.go +++ b/drivers/storage/portworx/util/util.go @@ -1533,3 +1533,75 @@ func ShouldUseClusterDomain(node *api.StorageNode) (bool, error) { } return true, nil } + +func PopulateSecurityEnvironmentVariables( + cluster *corev1.StorageCluster, + envMap map[string]*v1.EnvVar, +) { + if !SecurityEnabled(cluster) { + return + } + envMap[EnvKeyPortworxAuthJwtSharedSecret] = &v1.EnvVar{ + Name: EnvKeyPortworxAuthJwtSharedSecret, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: *cluster.Spec.Security.Auth.SelfSigned.SharedSecret, + }, + Key: SecuritySharedSecretKey, + }, + }, + } + envMap[EnvKeyPortworxAuthSystemKey] = &v1.EnvVar{ + Name: EnvKeyPortworxAuthSystemKey, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: SecurityPXSystemSecretsSecretName, + }, + Key: SecuritySystemSecretKey, + }, + }, + } + envMap[EnvKeyPortworxAuthJwtIssuer] = &v1.EnvVar{ + Name: EnvKeyPortworxAuthJwtIssuer, + Value: *cluster.Spec.Security.Auth.SelfSigned.Issuer, + } + pxVersion := GetPortworxVersion(cluster) + storkVersion := GetStorkVersion(cluster) + pxAppsIssuerVersion, err := version.NewVersion("2.6.0") + if err != nil { + logrus.Errorf("failed to create PX version variable 2.6.0: %s", err.Error()) + } + storkIssuerVersion, err := version.NewVersion("2.5.0") + if err != nil { + logrus.Errorf("failed to create Stork version variable 2.5.0: %s", err.Error()) + } + // apps issuer was added in PX version 2.6.0 + if pxVersion.GreaterThanOrEqual(pxAppsIssuerVersion) && storkVersion.GreaterThanOrEqual(storkIssuerVersion) { + envMap[EnvKeyPortworxAuthSystemAppsKey] = &v1.EnvVar{ + Name: EnvKeyPortworxAuthSystemAppsKey, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: SecurityPXSystemSecretsSecretName, + }, + Key: SecurityAppsSecretKey, + }, + }, + } + } else { + // otherwise, use the stork issuer for pre-2.6 support + envMap[EnvKeyPortworxAuthStorkKey] = &v1.EnvVar{ + Name: EnvKeyPortworxAuthStorkKey, + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: SecurityPXSystemSecretsSecretName, + }, + Key: SecurityAppsSecretKey, + }, + }, + } + } +} diff --git a/go.mod b/go.mod index 12de8bca47..0693f7daf8 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,12 @@ require ( github.com/go-logr/logr v1.3.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/mock v1.6.0 + github.com/golang/protobuf v1.5.4 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.6.0 github.com/libopenstorage/cloudops v0.0.0-20221107233229-3fa4664e96b1 + github.com/libopenstorage/grpc-framework v0.1.4 github.com/libopenstorage/openstorage v9.4.47+incompatible github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 github.com/pborman/uuid v1.2.1 @@ -19,12 +21,13 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.63.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.46.0 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.12 - golang.org/x/sys v0.16.0 - google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 - google.golang.org/grpc v1.60.1 - google.golang.org/protobuf v1.32.0 + golang.org/x/net v0.27.0 + golang.org/x/sys v0.22.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.27.1 k8s.io/apiextensions-apiserver v0.26.5 @@ -45,11 +48,12 @@ require ( require ( github.com/armon/go-metrics v0.4.1 // indirect 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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect github.com/codeskyblue/go-sh v0.0.0-20170112005953-b097669b1569 // indirect github.com/coreos/etcd v3.3.13+incompatible // indirect github.com/coreos/go-oidc v2.2.1+incompatible // indirect + github.com/coreos/go-oidc/v3 v3.11.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -59,17 +63,19 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.3 // 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 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/consul/api v1.20.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.4.0 // indirect @@ -85,7 +91,6 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -97,25 +102,24 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect - github.com/prometheus/client_golang v1.15.1 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/client_golang v1.19.1 // 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/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.13.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 v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7f810ceff3..aece394f5a 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,7 @@ cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5x cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM= cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= @@ -330,11 +331,14 @@ cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdi cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -1763,8 +1767,9 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/chavacava/garif v0.0.0-20220630083739-93517212f375/go.mod h1:4m1Rv7xfuwWPNKXlThldNuJvutYM6J95wNuuVmn55To= @@ -1995,6 +2000,8 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -2340,6 +2347,8 @@ github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I github.com/go-ini/ini v1.66.6/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v4 v4.0.3 h1:o8aphO8Hv6RPmH+GfzVuyf7YXSBibp+8YyHdOoDESGo= +github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -2702,8 +2711,9 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -2858,8 +2868,9 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/cloud-bigtable-clients-test v0.0.0-20221104150409-300c96f7b1f5/go.mod h1:Udm7et5Lt9Xtzd4n07/kKP80IdlR4zVDjtlUZEO2Dd8= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= @@ -2983,6 +2994,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul-template v0.25.0/go.mod h1:/vUsrJvDuuQHcxEw0zik+YXTS7ZKWZjQeaQhshBmfH0= @@ -3500,6 +3513,8 @@ github.com/libopenstorage/external-storage v5.1.1-0.20190919185747-9394ee8dd536+ github.com/libopenstorage/gossip v0.0.0-20190507031959-c26073a01952/go.mod h1:TjXt2Iz2bTkpfc4Q6xN0ttiNipTVwEEYoZSMZHlfPek= github.com/libopenstorage/gossip v0.0.0-20200808224301-d5287c7c8b24/go.mod h1:TjXt2Iz2bTkpfc4Q6xN0ttiNipTVwEEYoZSMZHlfPek= github.com/libopenstorage/gossip v0.0.0-20220309192431-44c895e0923e/go.mod h1:TjXt2Iz2bTkpfc4Q6xN0ttiNipTVwEEYoZSMZHlfPek= +github.com/libopenstorage/grpc-framework v0.1.4 h1:0IEQ73UE5kc3GukW0qwL0aCp7QKPEM+PJgau81NIGZo= +github.com/libopenstorage/grpc-framework v0.1.4/go.mod h1:ifKXukHbd75tqTb4NXgnofNlyr5CHeIg/NI7bSHG9aU= github.com/libopenstorage/openstorage v1.0.1-0.20240221210452-7757fdc2b8ff h1:9uognDSvafpcrNICT8I5OJRt9TlLeV61cF6nIl9KwBQ= github.com/libopenstorage/openstorage v1.0.1-0.20240221210452-7757fdc2b8ff/go.mod h1:8E8ueY3NJV+tcOr1BQBvyNU9FRtDfcRGB7+trr07+rA= github.com/libopenstorage/openstorage-sdk-clients v0.109.0/go.mod h1:vo0c/nLG2HIyQva4Avwx61U1kWcw4HGQh3sjzV2DEEs= @@ -3642,7 +3657,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= @@ -4140,8 +4154,9 @@ github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrb github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -4151,8 +4166,9 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +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.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= @@ -4180,8 +4196,9 @@ github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRL github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.38.0/go.mod h1:MBXfmBQZrK5XpbCkjofnXs96LD2QQ7fEq4C0xjC/yec= github.com/prometheus/common v0.41.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/assets v0.1.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= @@ -4210,8 +4227,9 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +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/prometheus/prometheus v0.0.0-20180315085919-58e2a31db8de/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= github.com/prometheus/prometheus v0.0.0-20190818123050-43acd0e2e93f/go.mod h1:rMTlmxGCvukf2KMu3fClMDKLLoJ5hl61MhcJ7xKakf0= github.com/prometheus/prometheus v0.35.0/go.mod h1:7HaLx5kEPKJ0GDgbODG0fZgXbQ8K/XjZNJXQmbmgQlY= @@ -4288,6 +4306,8 @@ github.com/rs/cors v1.6.1-0.20190116175910-76f58f330d76/go.mod h1:gFx+x8UowdsKA9 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -4481,8 +4501,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -4502,8 +4523,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= @@ -5017,8 +5039,9 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -5097,8 +5120,9 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20171107184841-a337091b0525/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -5223,8 +5247,9 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -5271,8 +5296,9 @@ golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQ golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -5515,8 +5541,9 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -5539,8 +5566,9 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.1.1-0.20171102192421-88f656faf3f3/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -5565,8 +5593,9 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -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.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -5713,7 +5742,6 @@ google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -5936,8 +5964,9 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go. google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= @@ -5961,8 +5990,9 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -5988,8 +6018,9 @@ google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.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/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= diff --git a/pkg/apis/core/v1/storagecluster.go b/pkg/apis/core/v1/storagecluster.go index 9ac028bf00..8cc5486df5 100644 --- a/pkg/apis/core/v1/storagecluster.go +++ b/pkg/apis/core/v1/storagecluster.go @@ -103,6 +103,8 @@ type StorageClusterSpec struct { // to the storage driver. The autopilot component could augment the storage // driver to take intelligent actions based on the current state of the cluster. Autopilot *AutopilotSpec `json:"autopilot,omitempty"` + // ResourceGateway limits Px access to external resources to prevent throttling + ResourceGateway *ResourceGatewaySpec `json:"resourceGateway,omitempty"` // Monitoring contains monitoring configuration for the storage cluster. Monitoring *MonitoringSpec `json:"monitoring,omitempty"` // Security configurations for setting up an auth enabled or disabled cluster @@ -486,6 +488,18 @@ type AutopilotSpec struct { Resources *v1.ResourceRequirements `json:"resources,omitempty"` } +// ResourceGatewaySpec contains details of an ResourceGateway component +type ResourceGatewaySpec struct { + // Enabled decides whether ResourceGateway needs to be enabled + Enabled bool `json:"enabled,omitempty"` + // Image is docker image of the ResourceGateway + Image string `json:"image,omitempty"` + // Args is a map of arguments given to ResourceGateway + Args map[string]string `json:"args,omitempty"` + // Resources requests and limits (like CPU and memory) for ResourceGateway pod + Resources *v1.ResourceRequirements `json:"resources,omitempty"` +} + // DataProviderSpec contains the details for data providers for components like autopilot type DataProviderSpec struct { // Name is the unique name for the provider @@ -622,6 +636,7 @@ type ComponentImages struct { Pause string `json:"pause,omitempty"` DynamicPlugin string `json:"dynamicPlugin,omitempty"` DynamicPluginProxy string `json:"dynamicPluginProxy,omitempty"` + ResourceGateway string `json:"resourceGateway,omitempty"` } // Storage represents cluster storage details diff --git a/pkg/apis/core/v1/zz_generated.deepcopy.go b/pkg/apis/core/v1/zz_generated.deepcopy.go index d9dcb973e6..292e89552c 100644 --- a/pkg/apis/core/v1/zz_generated.deepcopy.go +++ b/pkg/apis/core/v1/zz_generated.deepcopy.go @@ -793,6 +793,34 @@ func (in *PrometheusSpec) DeepCopy() *PrometheusSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceGatewaySpec) DeepCopyInto(out *ResourceGatewaySpec) { + *out = *in + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceGatewaySpec. +func (in *ResourceGatewaySpec) DeepCopy() *ResourceGatewaySpec { + if in == nil { + return nil + } + out := new(ResourceGatewaySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RollingUpdateStorageCluster) DeepCopyInto(out *RollingUpdateStorageCluster) { *out = *in @@ -1036,6 +1064,11 @@ func (in *StorageClusterSpec) DeepCopyInto(out *StorageClusterSpec) { *out = new(AutopilotSpec) (*in).DeepCopyInto(*out) } + if in.ResourceGateway != nil { + in, out := &in.ResourceGateway, &out.ResourceGateway + *out = new(ResourceGatewaySpec) + (*in).DeepCopyInto(*out) + } if in.Monitoring != nil { in, out := &in.Monitoring, &out.Monitoring *out = new(MonitoringSpec) diff --git a/pkg/constants/metadata.go b/pkg/constants/metadata.go index e00083086a..3f2d2cdf87 100644 --- a/pkg/constants/metadata.go +++ b/pkg/constants/metadata.go @@ -50,8 +50,10 @@ const ( // OperatorLabelManagedByKey is a label key that is added to any object that is // managed the Portworx operator. OperatorLabelManagedByKey = OperatorPrefix + "/managed-by" - // OperatorLabelManagedByValue indicates that the object is managed by portworx. - OperatorLabelManagedByValue = "portworx" + // OperatorLabelManagedByValuePortworx indicates that the object is managed by portworx. + OperatorLabelManagedByValuePortworx = "portworx" + // OperatorLabelManagedByValueResourceGateway indicates that the object is managed by Px Resource Gateway. + OperatorLabelManagedByValueResourceGateway = "resource-gateway" ) const ( diff --git a/pkg/controller/storagecluster/controller_test.go b/pkg/controller/storagecluster/controller_test.go index 8dfc2b381a..6aa922eb0e 100644 --- a/pkg/controller/storagecluster/controller_test.go +++ b/pkg/controller/storagecluster/controller_test.go @@ -1829,7 +1829,7 @@ func TestStoragePodGetsScheduled(t *testing.T) { Labels: map[string]string{ constants.LabelKeyClusterName: cluster.Name, constants.LabelKeyDriverName: driverName, - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, }, Annotations: make(map[string]string), }, @@ -1947,7 +1947,7 @@ func TestStoragePodGetsScheduledK8s1_24(t *testing.T) { Labels: map[string]string{ constants.LabelKeyClusterName: cluster.Name, constants.LabelKeyDriverName: driverName, - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, }, Annotations: make(map[string]string), }, @@ -2338,7 +2338,7 @@ func TestStoragePodGetsScheduledWithCustomNodeSpecs(t *testing.T) { Labels: map[string]string{ constants.LabelKeyClusterName: cluster.Name, constants.LabelKeyDriverName: driverName, - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, }, }, Spec: expectedPodSpec, diff --git a/pkg/controller/storagecluster/kubevirt.go b/pkg/controller/storagecluster/kubevirt.go index c4a09b3f92..15df66a628 100644 --- a/pkg/controller/storagecluster/kubevirt.go +++ b/pkg/controller/storagecluster/kubevirt.go @@ -148,7 +148,7 @@ OUTER: } // All checks passed. Start the live-migration. labels := map[string]string{ - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, } annotations := map[string]string{ constants.AnnotationControllerRevisionHashKey: controllerRevisionHash, diff --git a/pkg/controller/storagecluster/kubevirt_test.go b/pkg/controller/storagecluster/kubevirt_test.go index b904d73870..35ee10e9c4 100644 --- a/pkg/controller/storagecluster/kubevirt_test.go +++ b/pkg/controller/storagecluster/kubevirt_test.go @@ -202,7 +202,7 @@ func TestStartEvictingVMPods(t *testing.T) { constants.AnnotationControllerRevisionHashKey: hash, } expectedLabels := map[string]string{ - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, } // Test case: no migration exists virtLauncherPod, vmi := getTestVirtLauncherPodAndVMI("virt-launcher-1", "node1") @@ -308,7 +308,7 @@ func TestStartEvictingVMPods(t *testing.T) { constants.AnnotationControllerRevisionHashKey: "different-hash", }, Labels: map[string]string{ - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, }, SourceNode: virtLauncherPod.Spec.NodeName, }, diff --git a/pkg/controller/storagecluster/storagecluster.go b/pkg/controller/storagecluster/storagecluster.go index 5e3c8af315..d5f1ea03a6 100644 --- a/pkg/controller/storagecluster/storagecluster.go +++ b/pkg/controller/storagecluster/storagecluster.go @@ -1395,7 +1395,7 @@ func (c *Controller) CreatePodTemplate( addPxServiceAccountTokenSecretIfNotExist(&podSpec) podSpec.NodeName = node.Name labels := c.StorageClusterSelectorLabels(cluster) - labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValue + labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValuePortworx newTemplate := v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Namespace: cluster.Namespace, diff --git a/pkg/controller/storagenode/storagenode.go b/pkg/controller/storagenode/storagenode.go index 31143bbc2a..6e9c560e05 100644 --- a/pkg/controller/storagenode/storagenode.go +++ b/pkg/controller/storagenode/storagenode.go @@ -3,11 +3,12 @@ package storagenode import ( "context" "fmt" + "strings" + "time" + "github.com/libopenstorage/openstorage/api" pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" "google.golang.org/grpc" - "strings" - "time" "github.com/hashicorp/go-version" apiextensionsops "github.com/portworx/sched-ops/k8s/apiextensions" @@ -379,11 +380,11 @@ func (c *Controller) syncStorage( } value, present := podCopy.Labels[constants.OperatorLabelManagedByKey] - if !present || value != constants.OperatorLabelManagedByValue { + if !present || value != constants.OperatorLabelManagedByValuePortworx { if podCopy.Labels == nil { podCopy.Labels = make(map[string]string) } - podCopy.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValue + podCopy.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValuePortworx updateNeeded = true } @@ -424,7 +425,7 @@ func (c *Controller) createKVDBPod( func (c *Controller) kvdbPodLabels(cluster *corev1.StorageCluster) map[string]string { podLabels := map[string]string{ - constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValue, + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValuePortworx, } for k, v := range c.kvdbOldPodLabels(cluster) { podLabels[k] = v diff --git a/pkg/resource-gateway/priority_queue.go b/pkg/resource-gateway/priority_queue.go new file mode 100644 index 0000000000..76700c8573 --- /dev/null +++ b/pkg/resource-gateway/priority_queue.go @@ -0,0 +1,141 @@ +// Implements a priority queue with priorities high, medium, and low defined in proto. +// The queue supports the following operations: Enqueue, Dequeue, Front, and Remove. +// +// It is implemented with a separate list for each priority. +// When a client is enqueued, it is added to the list corresponding to its priority. +// +// When the front of the queue is requested, +// the queue will return the client at the front of the highest priority non-empty list. +// +// The queue is configured with a maximum size to prevent unbounded growth. +// When the queue is full, Enqueue will return an error. +package resource_gateway + +import ( + "container/list" + "fmt" + "sync" + + pb "github.com/libopenstorage/operator/proto" +) + +var ( + // priorityDescendingOrder is the order of priorities in descending order. + // It is used to iterate through the queues in order of priority. + priorityDescendingOrder = []pb.AccessPriority_Type{ + pb.AccessPriority_HIGH, + pb.AccessPriority_MEDIUM, + pb.AccessPriority_LOW, + } +) + +// PriorityQueue is an interface for a priority queue. +type PriorityQueue interface { + // Enqueue adds a client to the queue with the given priority. + Enqueue(clientId string, priority pb.AccessPriority_Type) error + // Dequeue removes the client at the front of the queue with the given priority. + Dequeue(priority pb.AccessPriority_Type) error + // Front returns the client at the front of the non-empty queue with the highest priority. + Front() (clientId string, accessPriority pb.AccessPriority_Type) + // Remove removes the client from the priority queue. + Remove(clientId string) error +} + +// priorityQueue is an implementation of the PriorityQueue interface. +type priorityQueue struct { + // sync.Mutex protects Q operations from concurrent access + sync.Mutex + // queues for each priority + // uses list for O(1) implementation of Q operations + queues map[pb.AccessPriority_Type]*list.List + // maxQueueSize is the maximum number of elements across all queues + maxQueueSize uint +} + +// NewPriorityQueue creates a new priority queue with the given maximum size +// and returns a PriorityQueue interface. +func NewPriorityQueue(maxQueueSize uint) PriorityQueue { + queues := make(map[pb.AccessPriority_Type]*list.List) + for _, priority := range priorityDescendingOrder { + queues[priority] = list.New() + } + return &priorityQueue{ + queues: queues, + maxQueueSize: maxQueueSize, + } +} + +// Enqueue adds a client to the queue with the given priority. +// +// It returns an error if the queue is full that is +// total number of elements is equal to maxQueueSize. +func (pq *priorityQueue) Enqueue(clientId string, priority pb.AccessPriority_Type) error { + pq.Lock() + defer pq.Unlock() + + if pq.len() == pq.maxQueueSize { + return fmt.Errorf("queue is full") + } + + pq.queues[priority].PushBack(clientId) + return nil +} + +// Dequeue removes the client at the front of the queue with the given priority. +func (pq *priorityQueue) Dequeue(priority pb.AccessPriority_Type) error { + pq.Lock() + defer pq.Unlock() + + if pq.queues[priority].Front() == nil { + return fmt.Errorf("queue %v is empty", priority) + } + + pq.queues[priority].Remove(pq.queues[priority].Front()) + return nil +} + +// Front returns the client at the front of the priority queue. +// +// It iterates through the queues in descending order of priority +// and returns the front of the first non-empty queue. +func (pq *priorityQueue) Front() (string, pb.AccessPriority_Type) { + pq.Lock() + defer pq.Unlock() + + for _, priority := range priorityDescendingOrder { + if pq.queues[priority].Front() != nil { + return pq.queues[priority].Front().Value.(string), priority + } + } + return "", pb.AccessPriority_LOW +} + +// Remove removes the client from the priority queue. +// +// It iterates through all queues to find the client and removes it. +func (pq *priorityQueue) Remove(clientId string) error { + pq.Lock() + defer pq.Unlock() + + for _, q := range pq.queues { + for e := q.Front(); e != nil; e = e.Next() { + if e.Value.(string) == clientId { + q.Remove(e) + return nil + } + } + } + return fmt.Errorf("client %v not found in the queue", clientId) +} + +// len returns the total number of elements across all queues +// +// It iterates through all queues and returns the sum of their lengths. +// It should be called with the mutex locked. +func (pq *priorityQueue) len() uint { + var length uint + for _, q := range pq.queues { + length += uint(q.Len()) + } + return length +} diff --git a/pkg/resource-gateway/resource_gateway_server.go b/pkg/resource-gateway/resource_gateway_server.go new file mode 100644 index 0000000000..f4bf318d36 --- /dev/null +++ b/pkg/resource-gateway/resource_gateway_server.go @@ -0,0 +1,116 @@ +package resource_gateway + +import ( + "fmt" + "os" + "time" + + "github.com/libopenstorage/grpc-framework/pkg/util" + grpcFramework "github.com/libopenstorage/grpc-framework/server" + "github.com/libopenstorage/openstorage/pkg/sched" + pb "github.com/libopenstorage/operator/proto" + "github.com/portworx/sched-ops/k8s/core" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + healthpb "google.golang.org/grpc/health/grpc_health_v1" +) + +const ( + // defaultServerName is the default server name for resource-gateway + defaultServerName = "resource-gateway" + // defaultServerHost is the default server host for resource-gateway + defaultServerHost = "127.0.0.1" + // defaultServerPort is the default server port for resource-gateway + defaultServerPort = "50051" +) + +// ResourceGatewayServer is the main struct for resource-gateway gRPC server +// +// It contains the gRPC server instance and various component servers like semaphore and health check +type ResourceGatewayServer struct { + server *grpcFramework.Server + + semaphoreServer *semaphoreServer + healthCheckServer *health.Server +} + +// NewResourceGatewayServer creates a new resource-gateway gRPC server instance +func NewResourceGatewayServer( + resourceGatewayConfig *grpcFramework.ServerConfig, + semaphoreConfig *SemaphoreConfig, +) *ResourceGatewayServer { + // create component servers + healthCheckServer := health.NewServer() + semaphoreServer := NewSemaphoreServer(semaphoreConfig) + + // register the component servers with the gRPC server config + resourceGatewayConfig. + RegisterGrpcServers(func(gs *grpc.Server) { + pb.RegisterSemaphoreServiceServer(gs, semaphoreServer) + }). + RegisterGrpcServers(func(gs *grpc.Server) { + healthpb.RegisterHealthServer(gs, healthCheckServer) + }). + WithDefaultGenericRoleManager() + + // create the gRPC server instance + resourceGatewayGRPCServer, err := grpcFramework.New(resourceGatewayConfig) + if err != nil { + fmt.Printf("Unable to create server: %v", err) + os.Exit(1) + } + + resourceGatewayServer := &ResourceGatewayServer{ + server: resourceGatewayGRPCServer, + semaphoreServer: semaphoreServer, + healthCheckServer: healthCheckServer, + } + return resourceGatewayServer +} + +func NewResourceGatewayServerConfig() *grpcFramework.ServerConfig { + defaultServerAddress := fmt.Sprintf("%s:%s", defaultServerHost, defaultServerPort) + resourceGatewayServerConfig := &grpcFramework.ServerConfig{ + Name: defaultServerHost, + Address: defaultServerAddress, + AuditOutput: os.Stdout, + AccessOutput: os.Stdout, + } + return resourceGatewayServerConfig +} + +// SetupSigIntHandler sets up a signal handler to stop the server +func (r *ResourceGatewayServer) SetupSigIntHandler() error { + signal_handler := util.NewSigIntManager(func() { + r.server.Stop() + r.healthCheckServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING) + r.server.Address() + os.Exit(0) + }) + + return signal_handler.Start() +} + +// Start starts the resource-gateway gRPC server +func (r *ResourceGatewayServer) Start() error { + // Initialize the k8s client + _, err := core.Instance().GetVersion() + if err != nil { + return fmt.Errorf("Unable to get k8s version: %v", err) + } + + if sched.Instance() == nil { + sched.Init(time.Second) + } + + err = r.server.Start() + if err != nil { + return err + } + r.healthCheckServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) + + // Wait. The signal handler will exit cleanly + logrus.Info("Px gRPC server running") + select {} +} diff --git a/pkg/resource-gateway/semaphore.go b/pkg/resource-gateway/semaphore.go new file mode 100644 index 0000000000..0824e8e2ac --- /dev/null +++ b/pkg/resource-gateway/semaphore.go @@ -0,0 +1,399 @@ +// Package resource_gateway provides a semaphore implementation backed by priority queue +package resource_gateway + +import ( + "sync" + "time" + + "github.com/libopenstorage/openstorage/pkg/sched" + pb "github.com/libopenstorage/operator/proto" + "github.com/sirupsen/logrus" +) + +const ( + nullPermit uint32 = 0 +) + +var ( + testOverride = false +) + +type SemaphorePriorityQueue interface { + Acquire(clientId string, priority pb.AccessPriority_Type) (pb.AccessStatus_Type, error) + Release(clientId string) error + Heartbeat(clientId string) pb.AccessStatus_Type + + Update(config *SemaphoreConfig) +} + +type activeLeasesMap map[string]int64 + +// semaphorePriorityQueue implements the SemaphorePriorityQueue interface +type semaphorePriorityQueue struct { + // configurations + config *SemaphoreConfig + + // thread safety + mutex sync.Mutex + + // internal state + priorityQueue PriorityQueue + activeLeases activeLeasesMap + availablePermits uint + heartbeats map[string]int64 + + // persistent state + configMap *configMap + configMapUpdateDone chan struct{} +} + +// NewSemaphorePriorityQueue creates a new or loads an existing semaphore priority queue +// if the backing configmap does not exist then it creates a new one +func NewSemaphorePriorityQueueWithConfig(config *SemaphoreConfig) *semaphorePriorityQueue { + // create or update the configmap + configMap, err := createOrUpdateConfigMap(config) + if err != nil { + panic(err) + } + + semPQ := &semaphorePriorityQueue{ + priorityQueue: NewPriorityQueue(config.MaxQueueSize), + heartbeats: map[string]int64{}, + configMapUpdateDone: make(chan struct{}), + config: config, + configMap: configMap, + } + + semPQ.populateSemaphoreConfig() + semPQ.initPermitsAndLeases() + // TODO cancel and restart background tasks + semPQ.startBackgroundTasks() + + return semPQ +} + +// NewSemaphorePriorityQueue creates a new or loads an existing semaphore priority queue +// if the backing configmap does not exist then it creates a new one +func NewSemaphorePriorityQueueWithConfigMap(config *SemaphoreConfig) *semaphorePriorityQueue { + // create or update the configmap + configMap, err := getConfigMap(config) + if err != nil { + panic(err) + } + + semPQ := &semaphorePriorityQueue{ + priorityQueue: NewPriorityQueue(config.MaxQueueSize), + heartbeats: map[string]int64{}, + configMapUpdateDone: make(chan struct{}), + config: config, + configMap: configMap, + } + + semPQ.populateSemaphoreConfig() + semPQ.initPermitsAndLeases() + semPQ.startBackgroundTasks() + + return semPQ +} + +func (s *semaphorePriorityQueue) Update(config *SemaphoreConfig) { + s.mutex.Lock() + defer s.mutex.Unlock() + + logrus.Infof("Update semaphore config: %v", config) + s.config = config + s.configMap.update(config) + + // TODO: update the internal state based on the new config + s.populateSemaphoreConfig() + s.initPermitsAndLeases() +} + +// populateSemaphoreConfig fetches the latest copy of configmap from kubernetes +// and updates the values in the semaphore config +func (s *semaphorePriorityQueue) populateSemaphoreConfig() { + s.config.ConfigMapName = s.configMap.Name() + s.config.ConfigMapNamespace = s.configMap.Namespace() + s.config.ConfigMapLabels = s.configMap.Labels() + + nPermits, err := s.configMap.NPermits() + if err != nil { + panic(err) + } + s.config.NPermits = nPermits + + leaseTimeout, err := s.configMap.LeaseTimeout() + if err != nil { + panic(err) + } + s.config.LeaseTimeout = leaseTimeout +} + +// populate structures for active leases and available permits +func (s *semaphorePriorityQueue) initPermitsAndLeases() { + activeLeases, err := s.configMap.ActiveLeases() + if err != nil { + panic(err) + } + s.activeLeases = activeLeases + s.availablePermits = uint(s.config.NPermits) - uint(len(activeLeases)) +} + +// startBackgroundTasks starts background workers for updating the configmap, +// cleaning up dead clients and reclaiming expired leases +func (s *semaphorePriorityQueue) startBackgroundTasks() { + // TODO: cancel and restart background tasks + bgTasks := []struct { + name string + f func() + interval time.Duration + }{ + {"updateConfigMap", s.updateConfigMap, s.config.ConfigMapUpdatePeriod}, + {"cleanupDeadClients", s.cleanupDeadClients, s.config.DeadClientTimeout / 2}, + {"reclaimExpiredLeases", s.reclaimExpiredLeases, s.config.LeaseTimeout / 2}, + } + + for _, bgTask := range bgTasks { + f := bgTask.f + intv := bgTask.interval + if testOverride { + intv = time.Second + } + taskID, err := sched.Instance().Schedule( + func(_ sched.Interval) { f() }, + sched.Periodic(intv), + time.Now(), false, + ) + if err != nil { + panic(err) + } + logrus.Debugf("Scheduled task %v with interval %v and Id %v", + bgTask.name, bgTask.interval, taskID) + } +} + +// Acquire acquires a lease for the client with the given priority +func (s *semaphorePriorityQueue) Acquire(clientId string, priority pb.AccessPriority_Type) (pb.AccessStatus_Type, error) { + s.mutex.Lock() + updateDone := false + defer func() { + s.mutex.Unlock() + if updateDone { + <-s.configMapUpdateDone + } + }() + logrus.Debugf("Received Acquire request for client %v", clientId) + + // check if the client already has a lease, if yes return + if s.hasActiveLease(clientId) { + logrus.Debugf("Already acquired lease for client %v", clientId) + return pb.AccessStatus_LEASED, nil + } + + // check if the client is already in the queue, if not add it + // no heartbeat = new client + if _, isQueued := s.heartbeats[clientId]; !isQueued { + logrus.Debugf("Enqueueing client %v with priority %v", clientId, priority) + err := s.priorityQueue.Enqueue(clientId, priority) + if err != nil { + return pb.AccessStatus_TYPE_UNSPECIFIED, err + } + } + + // update the heartbeat of the client + s.heartbeats[clientId] = time.Now().Unix() + + // try to acquire the lease + if hasAcquiredLease := s.tryAcquire(clientId); hasAcquiredLease { + logrus.Infof("Acquired lease for client %v", clientId) + updateDone = true + return pb.AccessStatus_LEASED, nil + } + + logrus.Debugf("Client %v is waiting in queue", clientId) + return pb.AccessStatus_QUEUED, nil +} + +// removeActiveLease removes the client from the heartbeats and activeLeases map +// and adds the respective permit back to the available permits list +// +// removeActiveLease should be called with mutex locked +func (s *semaphorePriorityQueue) removeActiveLease(clientId string) { + delete(s.heartbeats, clientId) + delete(s.activeLeases, clientId) + s.availablePermits++ +} + +// Release releases the lease held by the client +func (s *semaphorePriorityQueue) Release(clientId string) error { + s.mutex.Lock() + updateDone := false + defer func() { + s.mutex.Unlock() + if updateDone { + <-s.configMapUpdateDone + } + }() + logrus.Debugf("Received Release request for client %v", clientId) + + // check if the client has an active lease, if not return + if !s.hasActiveLease(clientId) { + logrus.Warnf("Did NOT find an active lease for the client %v!", clientId) + return nil + } + + s.removeActiveLease(clientId) + updateDone = true + + return nil +} + +// Heartbeat updates the heartbeat of the client and returns the status of the client +func (s *semaphorePriorityQueue) Heartbeat(clientId string) pb.AccessStatus_Type { + s.mutex.Lock() + defer s.mutex.Unlock() + + logrus.Debugf("Received Heartbeat request for client %v", clientId) + _, exists := s.heartbeats[clientId] + if !exists { + return pb.AccessStatus_TYPE_UNSPECIFIED + } + s.heartbeats[clientId] = time.Now().Unix() + + if s.hasActiveLease(clientId) { + return pb.AccessStatus_LEASED + } + return pb.AccessStatus_QUEUED +} + +// tryAcquire checks if a given client is at the front of the queue and if there is an available permit, +// if true, it assigns the permit to the client +// +// tryAcquire should be called with mutex locked +func (s *semaphorePriorityQueue) tryAcquire(clientId string) bool { + // check if the client is at the front of the queue + nextResouceId, priority := s.priorityQueue.Front() + if nextResouceId == "" { + panic("Queue is empty") + } + if nextResouceId != clientId { + return false + } + logrus.Debugf("Next resource in queue: %v", nextResouceId) + + // check if any permit is available + if s.availablePermits == 0 { + return false + } + s.availablePermits-- + + // remove the client from the queue and assign it the permit + err := s.priorityQueue.Dequeue(priority) + if err != nil { + panic(err) + } + s.activeLeases[clientId] = time.Now().Unix() + + return true +} + +// updateConfigMap updates the configmap with the active lease data in memory +// and notifies all the goroutines waiting on the configMapUpdateDone channel +// +// updateConfigMap is scheduled as a background task +func (s *semaphorePriorityQueue) updateConfigMap() { + + s.mutex.Lock() + defer s.mutex.Unlock() + logrus.Debugf("Running updateConfigMap background task") + + err := s.configMap.UpdateLeases(s.activeLeases) + if err != nil { + logrus.Fatalf("Failed to update configmap: %v", err) + } + + // notify all the goroutines waiting that the update is done + close(s.configMapUpdateDone) + // reset the channel for the next batch of waiters + s.configMapUpdateDone = make(chan struct{}) +} + +// cleanupDeadClients removes the dead clients from the priority queue +// and releases the leases held by them +// +// A client is considered dead when it has not sent a heartbeat +// for more than DeadClientTimeout duration +// +// cleanupDeadClients is scheduled as a background task +func (s *semaphorePriorityQueue) cleanupDeadClients() { + deadClients := []string{} + + s.mutex.Lock() + defer func() { + s.mutex.Unlock() + if len(deadClients) != 0 { + <-s.configMapUpdateDone // wait for the next update to complete + } + }() + logrus.Debugf("Running cleanupDeadClients background task") + + for clientId, lastHeartbeat := range s.heartbeats { + if time.Since(time.Unix(lastHeartbeat, 0)) > s.config.DeadClientTimeout { + deadClients = append(deadClients, clientId) + } + } + if len(deadClients) == 0 { + return + } + + logrus.Warnf("Cleaning up dead clients: %v", deadClients) + for _, clientId := range deadClients { + if s.hasActiveLease(clientId) { + s.removeActiveLease(clientId) + } else { + delete(s.heartbeats, clientId) + err := s.priorityQueue.Remove(clientId) + if err != nil { + logrus.Errorf("Failed to remove dead client %v from the queue: %v", clientId, err) + } + } + } +} + +// reclaimExpiredLeases releases the leases that have been held +// for more than LeaseTimeout duration +// +// reclaimExpiredLeases is scheduled as a background task +func (s *semaphorePriorityQueue) reclaimExpiredLeases() { + expiredLeases := []string{} + + s.mutex.Lock() + defer func() { + s.mutex.Unlock() + if len(expiredLeases) != 0 { + <-s.configMapUpdateDone // wait for the next update to complete + } + }() + + logrus.Debugf("Running reclaimExpiredLeases background task") + + for clientId, leaseTimeAcquired := range s.activeLeases { + timeSinceAcquire := time.Since(time.Unix(leaseTimeAcquired, 0)) + if timeSinceAcquire > s.config.LeaseTimeout { + expiredLeases = append(expiredLeases, clientId) + } + } + if len(expiredLeases) == 0 { + return + } + + logrus.Warnf("Reclaiming expired leases: %v", expiredLeases) + for _, clientId := range expiredLeases { + s.removeActiveLease(clientId) + } +} + +func (s *semaphorePriorityQueue) hasActiveLease(clientId string) bool { + _, hasLease := s.activeLeases[clientId] + return hasLease +} diff --git a/pkg/resource-gateway/semaphore_config.go b/pkg/resource-gateway/semaphore_config.go new file mode 100644 index 0000000000..d4897da3bf --- /dev/null +++ b/pkg/resource-gateway/semaphore_config.go @@ -0,0 +1,63 @@ +package resource_gateway + +import "time" + +const ( + // ResourceGatewayStr is the common string for resource-gateway components + resourceGatewayStr = "resource-gateway" + // defaultNamespace is the default namespace to create semaphore configmap + defaultNamespace = "kube-system" + // defaultConfigMapName is the default name for semaphore configmap + defaultConfigMapName = resourceGatewayStr + // defaultConfigMapLabels are the default labels applied to semaphore configmap + defaultConfigMapLabels = "name=resource-gateway" + // defaultConfigMapUpdatePeriod is the default time period between configmap updates + defaultConfigMapUpdatePeriod = 1 * time.Second + // defaultLeaseTimeout is the default time period after which a lease will force expire + defaultLeaseTimeout = 30 * time.Second + // defaultDeadClientTimeout is the default time period after which a node + // is considered dead if no heartbeats were received in this duration + defaultDeadClientTimeout = 10 * time.Second + // defaultMaxQueueSize is the default max queue size for semaphore server + defaultMaxQueueSize = 5000 +) + +type SemaphoreConfig struct { + NPermits uint32 + ConfigMapName string + ConfigMapNamespace string + ConfigMapLabels map[string]string + ConfigMapUpdatePeriod time.Duration + LeaseTimeout time.Duration + DeadClientTimeout time.Duration + MaxQueueSize uint +} + +func NewSemaphoreConfig() *SemaphoreConfig { + return &SemaphoreConfig{ + ConfigMapNamespace: defaultNamespace, + ConfigMapName: defaultConfigMapName, + ConfigMapLabels: map[string]string{"name": resourceGatewayStr}, + ConfigMapUpdatePeriod: defaultConfigMapUpdatePeriod, + LeaseTimeout: defaultLeaseTimeout, + DeadClientTimeout: defaultDeadClientTimeout, + MaxQueueSize: defaultMaxQueueSize, + } +} + +func copySemaphoreConfig(semaphoreConfig *SemaphoreConfig) *SemaphoreConfig { + configMapLabels := make(map[string]string) + for k, v := range semaphoreConfig.ConfigMapLabels { + configMapLabels[k] = v + } + return &SemaphoreConfig{ + NPermits: semaphoreConfig.NPermits, + ConfigMapName: semaphoreConfig.ConfigMapName, + ConfigMapNamespace: semaphoreConfig.ConfigMapNamespace, + ConfigMapLabels: configMapLabels, + ConfigMapUpdatePeriod: semaphoreConfig.ConfigMapUpdatePeriod, + LeaseTimeout: semaphoreConfig.LeaseTimeout, + DeadClientTimeout: semaphoreConfig.DeadClientTimeout, + MaxQueueSize: semaphoreConfig.MaxQueueSize, + } +} diff --git a/pkg/resource-gateway/semaphore_configmap.go b/pkg/resource-gateway/semaphore_configmap.go new file mode 100644 index 0000000000..da8f639c0b --- /dev/null +++ b/pkg/resource-gateway/semaphore_configmap.go @@ -0,0 +1,249 @@ +package resource_gateway + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/portworx/sched-ops/k8s/core" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + activeLeasesKey = "activeLeases" + nPermitsKey = "nPermits" + configMapUpdatePeriodKey = "configMapUpdatePeriod" + leaseTimeoutKey = "leaseTimeout" + deadClientTimeoutKey = "deadClientTimeout" + maxQueueSizeKey = "maxQueueSize" +) + +type configMap struct { + // activeLeases is the cache for fetching active leases persisted in configmap + activeLeases activeLeasesMap + cm *corev1.ConfigMap +} + +func createOrUpdateConfigMap(config *SemaphoreConfig) (*configMap, error) { + remoteConfigMap, err := updateConfigMap(config) + if err != nil && !k8s_errors.IsNotFound(err) { + return nil, err + } + + if k8s_errors.IsNotFound(err) { + // create a new configmap + remoteConfigMap = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.ConfigMapName, + Namespace: config.ConfigMapNamespace, + Labels: config.ConfigMapLabels, + }, + Data: map[string]string{ + activeLeasesKey: "", + nPermitsKey: fmt.Sprintf("%d", config.NPermits), + configMapUpdatePeriodKey: config.ConfigMapUpdatePeriod.String(), + leaseTimeoutKey: config.LeaseTimeout.String(), + deadClientTimeoutKey: config.DeadClientTimeout.String(), + maxQueueSizeKey: fmt.Sprintf("%d", config.MaxQueueSize), + }, + } + + remoteConfigMap, err = core.Instance().CreateConfigMap(remoteConfigMap) + if err != nil && !k8s_errors.IsAlreadyExists(err) { + return nil, err + } + } + + cm := &configMap{ + cm: remoteConfigMap, + } + cm.activeLeases, err = cm.ActiveLeases() + if err != nil { + return nil, err + } + return cm, nil +} + +func updateRemoteConfigMap(remoteConfigMap *corev1.ConfigMap, config *SemaphoreConfig) { + // update the configmap with the latest values + remoteConfigMap.Data[nPermitsKey] = fmt.Sprintf("%d", config.NPermits) + remoteConfigMap.Data[configMapUpdatePeriodKey] = config.ConfigMapUpdatePeriod.String() + remoteConfigMap.Data[leaseTimeoutKey] = config.LeaseTimeout.String() + remoteConfigMap.Data[deadClientTimeoutKey] = config.DeadClientTimeout.String() + remoteConfigMap.Data[maxQueueSizeKey] = strconv.FormatUint(uint64(config.MaxQueueSize), 10) +} + +// create the configmap if it doesn't exist then, fetch the latest copy of configmap and, +// update semaphore config values (nPermits, leaseTimeout) +func updateConfigMap(config *SemaphoreConfig) (*corev1.ConfigMap, error) { + remoteConfigMap, err := core.Instance().GetConfigMap(config.ConfigMapName, config.ConfigMapNamespace) + if err != nil { + return nil, err + } + + // update the configmap with the latest values + updateRemoteConfigMap(remoteConfigMap, config) + remoteConfigMap, err = core.Instance().UpdateConfigMap(remoteConfigMap) + if err != nil { + return nil, err + } + return remoteConfigMap, nil +} + +func getConfigMap(config *SemaphoreConfig) (*configMap, error) { + remoteConfigMap, err := core.Instance().GetConfigMap(config.ConfigMapName, config.ConfigMapNamespace) + if err != nil { + return nil, err + } + + configMapUpdatePeriod, err := time.ParseDuration(remoteConfigMap.Data[configMapUpdatePeriodKey]) + if err != nil { + return nil, err + } + + leaseTimeout, err := time.ParseDuration(remoteConfigMap.Data[leaseTimeoutKey]) + if err != nil { + return nil, err + } + + deadClientTimeout, err := time.ParseDuration(remoteConfigMap.Data[deadClientTimeoutKey]) + if err != nil { + return nil, err + } + + maxQueueSize, err := strconv.ParseUint(remoteConfigMap.Data[maxQueueSizeKey], 10, 32) + if err != nil { + return nil, err + } + + config.ConfigMapLabels = remoteConfigMap.Labels + config.ConfigMapUpdatePeriod = configMapUpdatePeriod + config.LeaseTimeout = leaseTimeout + config.DeadClientTimeout = deadClientTimeout + config.MaxQueueSize = uint(maxQueueSize) + + cm := &configMap{ + cm: remoteConfigMap, + } + cm.activeLeases, err = cm.ActiveLeases() + if err != nil { + return nil, err + } + return cm, nil +} + +func (c *configMap) update(config *SemaphoreConfig) error { + updateRemoteConfigMap(c.cm, config) + remoteConfigMap, err := core.Instance().UpdateConfigMap(c.cm) + if err != nil { + return err + } + c.cm = remoteConfigMap + return nil +} + +func (c *configMap) Name() string { + return c.cm.Name +} + +func (c *configMap) Namespace() string { + return c.cm.Namespace +} + +func (c *configMap) Labels() map[string]string { + return c.cm.Labels +} + +func (c *configMap) NPermits() (uint32, error) { + nPermits, err := strconv.Atoi(c.cm.Data[nPermitsKey]) + if err != nil { + return 0, err + } + return uint32(nPermits), nil +} + +func (c *configMap) LeaseTimeout() (time.Duration, error) { + leaseTimeout, err := time.ParseDuration(c.cm.Data[leaseTimeoutKey]) + if err != nil { + return 0, err + } + return leaseTimeout, nil +} + +func (c *configMap) DeadClientTimeout() (time.Duration, error) { + deadClientTimeout, err := time.ParseDuration(c.cm.Data[deadClientTimeoutKey]) + if err != nil { + return 0, err + } + return deadClientTimeout, nil +} + +func (c *configMap) ActiveLeases() (activeLeasesMap, error) { + if c.activeLeases == nil { + c.activeLeases = activeLeasesMap{} + activeLeasesValue := c.cm.Data[activeLeasesKey] + if activeLeasesValue != "" { + err := json.Unmarshal([]byte(activeLeasesValue), &c.activeLeases) + if err != nil { + return nil, err + } + } + } + + returnActiveLeases := activeLeasesMap{} + for key, val := range c.activeLeases { + returnActiveLeases[key] = val + } + return returnActiveLeases, nil +} + +// isConfigMapUpdateRequired compares two maps and returns true if they are different +func isConfigMapUpdateRequired(map1, map2 activeLeasesMap) bool { + if len(map1) != len(map2) { + return true + } + for key1, val1 := range map1 { + // TODO how are two structs compared + if val2, ok := map2[key1]; !ok || val1 != val2 { + logrus.Infof("Lease %s: %v != %v", key1, val1, val2) + return true + } + } + return false +} + +// Update replaces active leases in the configmap with the provided active leases +// it only makes an update call if the active leases have changed +func (c *configMap) UpdateLeases(newActiveLeases activeLeasesMap) error { + currentActiveLeases, err := c.ActiveLeases() + if err != nil { + panic(err) + } + if !isConfigMapUpdateRequired(newActiveLeases, currentActiveLeases) { + return nil + } + + logrus.Infof("Updating configmap: %v", newActiveLeases) + + // update the cache + c.activeLeases = activeLeasesMap{} + for key, val := range newActiveLeases { + c.activeLeases[key] = val + } + + activeLeasesValue, err := json.Marshal(newActiveLeases) + if err != nil { + return err + } + c.cm.Data[activeLeasesKey] = string(activeLeasesValue) + + c.cm, err = core.Instance().UpdateConfigMap(c.cm) + if err != nil { + return err + } + return nil +} diff --git a/pkg/resource-gateway/semaphore_server.go b/pkg/resource-gateway/semaphore_server.go new file mode 100644 index 0000000000..dbee501fc4 --- /dev/null +++ b/pkg/resource-gateway/semaphore_server.go @@ -0,0 +1,209 @@ +package resource_gateway + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/libopenstorage/operator/pkg/constants" + pb "github.com/libopenstorage/operator/proto" + "github.com/portworx/sched-ops/k8s/core" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type semaphoreServer struct { + semaphoreMap sync.Map + semaphoreConfig *SemaphoreConfig +} + +// NewSemaphoreServer creates a new semaphore server instance with the provided config. +func NewSemaphoreServer(semaphoreConfig *SemaphoreConfig) *semaphoreServer { + if semaphoreConfig.ConfigMapLabels == nil { + semaphoreConfig.ConfigMapLabels = make(map[string]string) + } + semaphoreConfig.ConfigMapLabels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValueResourceGateway + return &semaphoreServer{ + semaphoreConfig: semaphoreConfig, + } +} + +func (s *semaphoreServer) newSemaphoreConfig(req *pb.CreateRequest) *SemaphoreConfig { + // create a copy of the common semaphore config + semaphoreConfig := copySemaphoreConfig(s.semaphoreConfig) + + // update the config with the required request parameters + semaphoreConfig.NPermits = req.GetNPermits() + semaphoreConfig.ConfigMapName = fmt.Sprintf("%s-%s", semaphoreConfig.ConfigMapName, req.GetResourceId()) + + // override config values if optional request parameters are provided + // if req.GetConfigMapUpdatePeriod() > 0 { + // semaphoreConfig.ConfigMapUpdatePeriod = time.Duration(req.GetConfigMapUpdatePeriod()) * time.Second + // } + if req.GetLeaseTimeout() > 0 { + semaphoreConfig.LeaseTimeout = time.Duration(req.GetLeaseTimeout()) * time.Second + } + if req.GetDeadNodeTimeout() > 0 { + semaphoreConfig.DeadClientTimeout = time.Duration(req.GetDeadNodeTimeout()) * time.Second + } + // if req.GetMaxQueueSize() > 0 { + // semaphoreConfig.MaxQueueSize = req.GetMaxQueueSize() + // } + + return semaphoreConfig +} + +// Create implements the Create RPC method of the SemaphoreService. +// +// Create is used to create a new semaphore instance and its backing configmap. +// It validates that required fields are provided in the request, and an instance +// with the same resource ID does not already exist. +func (s *semaphoreServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) { + // validate request + if req.GetResourceId() == "" { + return &pb.CreateResponse{}, status.Error(codes.InvalidArgument, "Resource ID is required") + } + if req.GetNPermits() == 0 { + return &pb.CreateResponse{}, status.Error(codes.InvalidArgument, "Number of leases should be greater than 0") + } + semaphoreConfig := s.newSemaphoreConfig(req) + + // check if semaphore instance already exists + item, ok := s.semaphoreMap.Load(req.GetResourceId()) + if !ok { + logrus.Infof("Create semaphore with config: %v", semaphoreConfig) + semaphore := NewSemaphorePriorityQueueWithConfig(semaphoreConfig) + s.semaphoreMap.Store(req.GetResourceId(), semaphore) + return &pb.CreateResponse{}, nil + } + + semaphorePQ := item.(SemaphorePriorityQueue) + semaphorePQ.Update(semaphoreConfig) + return &pb.CreateResponse{}, status.Error(codes.AlreadyExists, "Resource already exists") + +} + +// Load loads the semaphore instances that are already created in the system. +// +// It fetches all the configmaps with the label managed by resource-gateway +// and creates semaphore instances for each of them. +func (s *semaphoreServer) Load() error { + configMapList, err := core.Instance().ListConfigMap( + s.semaphoreConfig.ConfigMapNamespace, + metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", + constants.OperatorLabelManagedByKey, constants.OperatorLabelManagedByValueResourceGateway), + }) + if err != nil { + return err + } + for _, remoteConfigMap := range configMapList.Items { + resourceId := remoteConfigMap.Name[len(s.semaphoreConfig.ConfigMapName+"-"):] + logrus.Infof("Loading semaphore for configmap %s and resource Id %s", remoteConfigMap.Name, resourceId) + // other config values will be populated later from the configMap data + semaphoreConfig := &SemaphoreConfig{ + ConfigMapName: remoteConfigMap.Name, + ConfigMapNamespace: remoteConfigMap.Namespace, + } + semaphore := NewSemaphorePriorityQueueWithConfigMap(semaphoreConfig) + s.semaphoreMap.Store(resourceId, semaphore) + } + return nil +} + +// Acquire implements the Acquire RPC method of the SemaphoreService. +// +// Acquire is used to acquire a lease on a resource. It validates that required +// fields are provided in the request, and the resource exists. It returns the +// status of the lease acquisition. +func (s *semaphoreServer) Acquire(ctx context.Context, req *pb.AcquireRequest) (*pb.AcquireResponse, error) { + // validate request + if req.GetResourceId() == "" { + return &pb.AcquireResponse{}, status.Error(codes.InvalidArgument, "Resource ID is required") + } + if req.GetClientId() == "" { + return &pb.AcquireResponse{}, status.Error(codes.InvalidArgument, "Client ID is required") + } + if req.GetAccessPriority() == pb.AccessPriority_TYPE_UNSPECIFIED { + return &pb.AcquireResponse{}, status.Error(codes.InvalidArgument, "Access Priority is required") + } + + // get the semaphore instance + item, ok := s.semaphoreMap.Load(req.GetResourceId()) + if !ok { + return &pb.AcquireResponse{}, status.Error(codes.NotFound, "Resource not found") + } + semaphorePQ := item.(SemaphorePriorityQueue) + + // process request to acquire lease + resourceState, err := semaphorePQ.Acquire(req.ClientId, req.AccessPriority) + if err != nil { + return &pb.AcquireResponse{}, status.Error(codes.Internal, err.Error()) + } + response := &pb.AcquireResponse{ + AccessStatus: resourceState, + } + return response, nil +} + +// Release implements the Release RPC method of the SemaphoreService. +// +// Release is used to release a lease on a resource. It validates that required +// fields are provided in the request, and the resource exists. +// It returns an empty response. +func (s *semaphoreServer) Release(ctx context.Context, req *pb.ReleaseRequest) (*pb.ReleaseResponse, error) { + // validate request + if req.GetResourceId() == "" { + return &pb.ReleaseResponse{}, status.Error(codes.InvalidArgument, "Resource ID is required") + } + if req.GetClientId() == "" { + return &pb.ReleaseResponse{}, status.Error(codes.InvalidArgument, "Client ID is required") + } + + // get the semaphore instance + item, ok := s.semaphoreMap.Load(req.GetResourceId()) + if !ok { + return &pb.ReleaseResponse{}, status.Error(codes.NotFound, "Resource not found") + } + semaphorePQ := item.(SemaphorePriorityQueue) + + // process request to release lease + err := semaphorePQ.Release(req.ClientId) + if err != nil { + return &pb.ReleaseResponse{}, status.Error(codes.Internal, err.Error()) + } + return &pb.ReleaseResponse{}, nil +} + +// Heartbeat implements the Heartbeat RPC method of the SemaphoreService. +// +// Heartbeat is used to keep the lease alive. It validates that required fields +// are provided in the request, and the resource exists. It returns the status +// of the lease. +func (s *semaphoreServer) Heartbeat(ctx context.Context, req *pb.HeartbeatRequest) (*pb.HeartbeatResponse, error) { + // validate request + if req.GetResourceId() == "" { + return &pb.HeartbeatResponse{}, status.Error(codes.InvalidArgument, "Resource ID is required") + } + if req.GetClientId() == "" { + return &pb.HeartbeatResponse{}, status.Error(codes.InvalidArgument, "Client ID is required") + } + + // get the semaphore instance + item, ok := s.semaphoreMap.Load(req.GetResourceId()) + if !ok { + return &pb.HeartbeatResponse{}, status.Error(codes.NotFound, "Resource not found") + } + semaphorePQ := item.(SemaphorePriorityQueue) + + // process client heartbeat + accessStatus := semaphorePQ.Heartbeat(req.ClientId) + response := &pb.HeartbeatResponse{ + AccessStatus: accessStatus, + } + return response, nil +} diff --git a/pkg/resource-gateway/semaphore_server_test.go b/pkg/resource-gateway/semaphore_server_test.go new file mode 100644 index 0000000000..3ee5d73699 --- /dev/null +++ b/pkg/resource-gateway/semaphore_server_test.go @@ -0,0 +1,117 @@ +package resource_gateway + +import ( + "context" + "fmt" + "testing" + + "github.com/libopenstorage/operator/pkg/constants" + pb "github.com/libopenstorage/operator/proto" + "github.com/portworx/sched-ops/k8s/core" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newSemaphoreServerTest() *semaphoreServer { + semaphoreConfig := &SemaphoreConfig{ + ConfigMapName: testConfigMapName, + ConfigMapNamespace: testConfigMapNamespace, + ConfigMapLabels: map[string]string{}, + ConfigMapUpdatePeriod: testConfigMapUpdatePeriod, + LeaseTimeout: testLeaseTimeout, + DeadClientTimeout: testDeadClientTimeout, + MaxQueueSize: 1000, + } + return NewSemaphoreServer(semaphoreConfig) +} + +func createTestConfigMap(t *testing.T, suffix string) { + configMapName := fmt.Sprintf("%s-%s", testConfigMapName, suffix) + _, err := core.Instance().CreateConfigMap(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: testConfigMapNamespace, + Labels: map[string]string{ + constants.OperatorLabelManagedByKey: constants.OperatorLabelManagedByValueResourceGateway, + }, + }, + Data: map[string]string{ + activeLeasesKey: "", + nPermitsKey: "1", + configMapUpdatePeriodKey: testConfigMapUpdatePeriod.String(), + leaseTimeoutKey: testLeaseTimeout.String(), + deadClientTimeoutKey: testDeadClientTimeout.String(), + maxQueueSizeKey: "1000", + }, + }) + require.NoError(t, err) +} + +func TestSemaphoreServer_Create(t *testing.T) { + setup(t) + + testResourceId := "resource-x" + deleteConfigMap(t, testResourceId) + s := newSemaphoreServerTest() + + // First create request for a semaphore will create a new configmap + configMapName := fmt.Sprintf("%s-%s", testConfigMapName, testResourceId) + req := &pb.CreateRequest{ + ResourceId: testResourceId, + NPermits: 1, + LeaseTimeout: 10, + } + _, err := s.Create(context.Background(), req) + require.NoError(t, err) + remoteConfigMap, err := core.Instance().GetConfigMap(configMapName, testConfigMapNamespace) + require.NoError(t, err) + require.Equal(t, "1", remoteConfigMap.Data[nPermitsKey]) + + // Second request for the same semaphore will update the existing configmap + req = &pb.CreateRequest{ + ResourceId: testResourceId, + NPermits: 2, + LeaseTimeout: 10, + } + _, err = s.Create(context.Background(), req) + require.Error(t, err) + require.Contains(t, err.Error(), "Resource already exists") + remoteConfigMap, err = core.Instance().GetConfigMap(configMapName, testConfigMapNamespace) + require.NoError(t, err) + require.Equal(t, "2", remoteConfigMap.Data[nPermitsKey]) +} + +func TestSemaphoreServer_Load(t *testing.T) { + setup(t) + + s := newSemaphoreServerTest() + deleteConfigMap(t, "resource-1") + deleteConfigMap(t, "resource-2") + + // Create configmaps for semaphore that should be loaded + createTestConfigMap(t, "resource-1") + createTestConfigMap(t, "resource-2") + + // Load the semaphore + err := s.Load() + require.NoError(t, err) + + req := &pb.AcquireRequest{ + ResourceId: "resource-1", + ClientId: "client-1", + AccessPriority: pb.AccessPriority_HIGH, + } + resp, err := s.Acquire(context.Background(), req) + require.NoError(t, err, "Unexpected error on Acquire") + require.Equal(t, pb.AccessStatus_LEASED, resp.GetAccessStatus()) + + req = &pb.AcquireRequest{ + ResourceId: "resource-2", + ClientId: "client-2", + AccessPriority: pb.AccessPriority_LOW, + } + resp, err = s.Acquire(context.Background(), req) + require.NoError(t, err, "Unexpected error on Acquire") + require.Equal(t, pb.AccessStatus_LEASED, resp.GetAccessStatus()) +} diff --git a/pkg/resource-gateway/semaphore_test.go b/pkg/resource-gateway/semaphore_test.go new file mode 100644 index 0000000000..b3a2885832 --- /dev/null +++ b/pkg/resource-gateway/semaphore_test.go @@ -0,0 +1,224 @@ +package resource_gateway + +import ( + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/libopenstorage/openstorage/pkg/sched" + pb "github.com/libopenstorage/operator/proto" + "github.com/portworx/sched-ops/k8s/core" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" +) + +const ( + // methods + noaction = "NoAction" + acquire = "Acquire" + release = "Release" + heartbeat = "Heartbeat" + + // priorities + nopriority = pb.AccessPriority_TYPE_UNSPECIFIED + low = pb.AccessPriority_LOW + med = pb.AccessPriority_MEDIUM + high = pb.AccessPriority_HIGH + + // access statuses + nostatus = pb.AccessStatus_TYPE_UNSPECIFIED + leased = pb.AccessStatus_LEASED + queued = pb.AccessStatus_QUEUED +) + +var ( + once sync.Once + + testConfigMapName = "resource-gateway-test" + testConfigMapNamespace = "kube-system" + testConfigMapUpdatePeriod = 1 * time.Second + testLeaseTimeout = 25 * time.Second + testDeadClientTimeout = 15 * time.Second +) + +func newSemaphorePriorityQueueTest() SemaphorePriorityQueue { + semaphoreConfig := &SemaphoreConfig{ + NPermits: uint32(2), + ConfigMapName: testConfigMapName, + ConfigMapNamespace: testConfigMapNamespace, + ConfigMapLabels: map[string]string{ + "name": testConfigMapName, + }, + ConfigMapUpdatePeriod: testConfigMapUpdatePeriod, + LeaseTimeout: testLeaseTimeout, + DeadClientTimeout: testDeadClientTimeout, + MaxQueueSize: 1000, + } + return NewSemaphorePriorityQueueWithConfig(semaphoreConfig) +} + +func deleteConfigMap(t *testing.T, suffix string) (err error) { + defer require.NoError(t, err, "Unable to delete configmap") + configMapName := testConfigMapName + if suffix != "" { + configMapName = fmt.Sprintf("%s-%s", testConfigMapName, suffix) + } + err = core.Instance().DeleteConfigMap(configMapName, testConfigMapNamespace) + if err != nil && !k8s_errors.IsNotFound(err) { + return err + } + + // wait for the configmap to be deleted and check every second upto 5s + for i := 0; i < 5; i++ { + _, err = core.Instance().GetConfigMap(configMapName, testConfigMapNamespace) + if k8s_errors.IsNotFound(err) { + return nil + } + time.Sleep(time.Second * 1) + } + return err +} + +func setup(t *testing.T) { + // init + once.Do(func() { + if sched.Instance() == nil { + sched.Init(time.Second) + } + + // validate kubeconfig is set + if os.Getenv("KUBECONFIG") == "" { + fmt.Println("KUBECONFIG not set. Cannot run Semaphore UT.") + return + } + logrus.SetLevel(logrus.DebugLevel) + testOverride = true + }) + + deleteConfigMap(t, "") // cleanup +} + +func validateConfigMap(t *testing.T, activeLeasesList []string) { + remoteConfigMap, err := core.Instance().GetConfigMap(testConfigMapName, testConfigMapNamespace) + cm := configMap{ + cm: remoteConfigMap, + } + require.NoError(t, err, "Unexpected error on get configmap") + activeLeases, err := cm.ActiveLeases() + require.NoError(t, err, "Unexpected error on fetch active leases from configmap") + require.Equal(t, len(activeLeasesList), len(activeLeases)) + for _, expectedClient := range activeLeasesList { + _, exists := activeLeases[expectedClient] + require.True(t, exists, "Client %s not found in configmap", expectedClient) + } +} + +func TestSemaphore_AcquireAndRelease(t *testing.T) { + + testCases := []struct { + name string + method string + delay time.Duration + client string + priority pb.AccessPriority_Type + status pb.AccessStatus_Type + activeLeasesList []string + }{ + // configMapUpdatePeriod = 1s, leaseTimeout = 25s, deadClientTimeout = 15s + // test acquire and release + // tests a client can take the first lease + {"client-1 is granted lease", acquire, 0, "client-1", low, leased, []string{"client-1"}}, + // 2 seconds elapsed + // tests a client can take the second / last lease + {"client-2 is granted lease", acquire, 0, "client-2", low, leased, []string{"client-1", "client-2"}}, + // 4 seconds elapsed + // tests a client can be queued to low priority + {"client-3 is queued to low", acquire, 0, "client-3", low, queued, []string{"client-1", "client-2"}}, + // tests a client can be queued to medium priority + {"client-4 is queued to med", acquire, 0, "client-4", med, queued, []string{"client-1", "client-2"}}, + // tests a client can be queued to high priority + {"client-5 is queued to high", acquire, 0, "client-5", high, queued, []string{"client-1", "client-2"}}, + // tests a client can be queued to high priority behind another high priority client + {"client-6 is queued to high", acquire, 0, "client-6", high, queued, []string{"client-1", "client-2"}}, + // tests the acquire will be noop and return correct status if the client already has the lease + {"client-1 is already leased", acquire, 0, "client-1", low, leased, []string{"client-1", "client-2"}}, + // tests the acquire will be noop if the client is already queued and no lease is available + {"client-5 cannot take the lease", acquire, 0, "client-5", high, queued, []string{"client-1", "client-2"}}, + // tests client is able to release the lease + {"client-1 releases the lease", release, 0, "client-1", nopriority, nostatus, []string{"client-2"}}, + // 6 seconds elapsed + // tests that heartbeats return the correct status + // heartbeat to keep client-2 lease alive till 21 seconds elapsed + {"client-2 heartbeats within lease timeout", heartbeat, 0, "client-2", nopriority, leased, []string{"client-2"}}, + // do acquire poll for clients in queue for implicit heartbeat + // keeps client 3 and 4 alive till 21 seconds elapsed + // tests the client with lower priority will NOT get the lease if a client with higher priority is in the queue + {"client-3 cannot take the lease", acquire, 0, "client-3", low, queued, []string{"client-2"}}, + {"client-4 cannot take the lease", acquire, 0, "client-4", med, queued, []string{"client-2"}}, + // tests the client with highest priority at the front of the queue will get the lease + {"client-5 is granted the lease", acquire, 0, "client-5", high, leased, []string{"client-5", "client-2"}}, + // 8 seconds elapsed + // keep client 6 alive till 23 seconds elapsed + {"client-6 cannot take the lease", acquire, 0, "client-6", high, queued, []string{"client-2", "client-5"}}, + {"client-5 releases the lease", release, 0, "client-5", nopriority, nostatus, []string{"client-2"}}, + // 10 seconds elapsed + {"client-6 is granted the lease", acquire, 0, "client-6", high, leased, []string{"client-2", "client-6"}}, + // 12 seconds elapsed + // tests that heartbeats return the correct status + {"client-2 heartbeats within lease timeout", heartbeat, 0, "client-2", nopriority, leased, []string{"client-2", "client-6"}}, + {"client-6 heartbeats", heartbeat, 0, "client-6", nopriority, leased, []string{"client-2", "client-6"}}, + {"client-6 releases the lease", release, 0, "client-6", nopriority, nostatus, []string{"client-2"}}, + // 14 seconds elapsed + // tests the client with medium priority will get the lease + {"client-4 is granted the lease", acquire, 0, "client-4", med, leased, []string{"client-2", "client-4"}}, + // 16 seconds elasped + // tests that client in queue that has not polled within dead client timeout will be removed the queue + // sleep 7 seconds + {"client-3 heartbeats outside dead client timeout", heartbeat, time.Second * 7, "client-3", nopriority, nostatus, []string{"client-2", "client-4"}}, + // 23 seconds elapsed + // tests that client that has held the lease for longer than the lease timeout will lose the lease + // sleep 5 seconds + {"client-2 heartbeats outside lease timeout", heartbeat, time.Second * 7, "client-2", nopriority, nostatus, []string{"client-4"}}, + // 30 seconds elapsed + // tests that client that has a lease and has not heartbeated within dead client timeout will lose the lease + // sleep 5 seconds + {"client-4 heartbeats outside dead client timeout", heartbeat, time.Second * 5, "client-4", nopriority, nostatus, []string{}}, + // 35 seconds elapsed + } + + setup(t) + semPQ := newSemaphorePriorityQueueTest() + + startTime := time.Now() + + for _, tc := range testCases { + if tc.delay > 0 { + time.Sleep(tc.delay) + fmt.Println("Elapsed time (after sleep): ", time.Since(startTime).Seconds()) + } + if tc.method == acquire { + t.Run(tc.name, func(t *testing.T) { + status, err := semPQ.Acquire(tc.client, tc.priority) + require.NoError(t, err, "Unexpected error on Acquire") + require.Equal(t, tc.status, status) + }) + } else if tc.method == release { + t.Run(tc.name, func(t *testing.T) { + err := semPQ.Release(tc.client) + require.NoError(t, err, "Unexpected error on Release") + }) + } else if tc.method == heartbeat { + t.Run(tc.name, func(t *testing.T) { + status := semPQ.Heartbeat(tc.client) + require.Equal(t, tc.status, status) + }) + } + + validateConfigMap(t, tc.activeLeasesList) + + fmt.Println("Elapsed time (after exec): ", time.Since(startTime).Seconds()) + } +} diff --git a/pkg/util/k8s/k8s.go b/pkg/util/k8s/k8s.go index d8e31391c5..900c2d75ed 100644 --- a/pkg/util/k8s/k8s.go +++ b/pkg/util/k8s/k8s.go @@ -1578,7 +1578,7 @@ func CreateOrUpdatePrometheus( if prometheus.Spec.PodMetadata.Labels == nil { prometheus.Spec.PodMetadata.Labels = make(map[string]string) } - prometheus.Spec.PodMetadata.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValue + prometheus.Spec.PodMetadata.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValuePortworx if modified || len(prometheus.OwnerReferences) > len(existingPrometheus.OwnerReferences) { prometheus.ResourceVersion = existingPrometheus.ResourceVersion logrus.Infof("Updating Prometheus %s/%s", prometheus.Namespace, prometheus.Name) @@ -1660,7 +1660,7 @@ func CreateOrUpdateAlertManager( if alertManager.Spec.PodMetadata.Labels == nil { alertManager.Spec.PodMetadata.Labels = make(map[string]string) } - alertManager.Spec.PodMetadata.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValue + alertManager.Spec.PodMetadata.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValuePortworx if modified || len(alertManager.OwnerReferences) > len(existingAlertManager.OwnerReferences) { alertManager.ResourceVersion = existingAlertManager.ResourceVersion logrus.Infof("Updating AlertManager %s/%s", alertManager.Namespace, alertManager.Name) @@ -2330,7 +2330,7 @@ func AddManagedByOperatorLabel(om metav1.ObjectMeta) metav1.ObjectMeta { if om.Labels == nil { om.Labels = make(map[string]string) } - om.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValue + om.Labels[constants.OperatorLabelManagedByKey] = constants.OperatorLabelManagedByValuePortworx return om } diff --git a/proto/Dockerfile b/proto/Dockerfile new file mode 100644 index 0000000000..ca40545140 --- /dev/null +++ b/proto/Dockerfile @@ -0,0 +1,10 @@ +# This Dockerfile is used to build base image for Resource Gateway proto files +# The image is based on osd-proto image provided by OpenStorage + +FROM quay.io/openstorage/osd-proto-clients + +LABEL author="dgoel" + +RUN mkdir -p /go/src/github.com/libopenstorage/operator/proto + +WORKDIR /go/src/github.com/libopenstorage/operator/proto diff --git a/proto/Makefile b/proto/Makefile new file mode 100644 index 0000000000..e87179a0c8 --- /dev/null +++ b/proto/Makefile @@ -0,0 +1,53 @@ +# set defaults +ifndef DOCKER_HUB_RESOURCE_GATEWAY_PROTO_IMG + DOCKER_HUB_RESOURCE_GATEWAY_PROTO_IMG := resource-gateway-proto + $(warning DOCKER_HUB_RESOURCE_GATEWAY_PROTO_IMG not defined, using '$(DOCKER_HUB_RESOURCE_GATEWAY_PROTO_IMG)' instead) +endif +ifndef DOCKER_HUB_RESOURCE_GATEWAY_PROTO_TAG + DOCKER_HUB_RESOURCE_GATEWAY_PROTO_TAG := latest + $(warning DOCKER_HUB_RESOURCE_GATEWAY_PROTO_TAG not defined, using '$(DOCKER_HUB_RESOURCE_GATEWAY_PROTO_TAG)' instead) +endif + +ifndef PROTOC +PROTOC = protoc +endif + +ifndef PROTOS_PATH +PROTOS_PATH = $(GOPATH)/src +endif + +ifndef PROTOSRC_PATH +PROTOSRC_PATH = $(PROTOS_PATH)/github.com/libopenstorage/operator/proto +endif + +RESOURCE_GATEWAY_PROTO_IMG=$(DOCKER_HUB_REPO)/$(DOCKER_HUB_RESOURCE_GATEWAY_PROTO_IMG):$(DOCKER_HUB_RESOURCE_GATEWAY_PROTO_TAG) + +# builds the container to compile proto files +build-docker: + docker build -t $(RESOURCE_GATEWAY_PROTO_IMG) . + +# builds the proto target inside the build container +docker-proto: + docker run \ + --privileged --rm -it \ + -v $(shell pwd):/go/src/github.com/libopenstorage/operator/proto \ + -e "GOPATH=/go" \ + -e "DOCKER_PROTO=yes" \ + -e "PATH=/bin:/usr/bin:/usr/local/bin:/go/bin:/usr/local/go/bin" \ + $(RESOURCE_GATEWAY_PROTO_IMG) \ + make proto + +# compiles the proto files - should be run inside the build container +proto: $(GOPATH)/bin/protoc-gen-go $(GOPATH)/bin/protoc-gen-grpc-gateway $(GOPATH)/bin/protoc-gen-swagger +ifndef DOCKER_PROTO + $(error Do not run directly. Run 'make docker-proto' instead.) +endif + @echo ">>> Generating protobuf definitions from api/api.proto" + $(PROTOC) -I $(PROTOSRC_PATH) \ + -I /usr/local/include \ + -I $(PROTOS_PATH)/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --go_out=plugins=grpc:. \ + $(PROTOSRC_PATH)/resource_gateway.proto + +clean: + rm -f resource_gateway.pb.go \ No newline at end of file diff --git a/proto/resource_gateway.pb.go b/proto/resource_gateway.pb.go new file mode 100644 index 0000000000..dab5676b39 --- /dev/null +++ b/proto/resource_gateway.pb.go @@ -0,0 +1,826 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: resource_gateway.proto + +package resourcegateway + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// Type of priority +type AccessPriority_Type int32 + +const ( + // Unspecified, do NOT use + AccessPriority_TYPE_UNSPECIFIED AccessPriority_Type = 0 + // Enqueued to low priority queue + AccessPriority_LOW AccessPriority_Type = 1 + // Enqueued to medium priority queue + AccessPriority_MEDIUM AccessPriority_Type = 2 + // Enqueued to high priority queue + AccessPriority_HIGH AccessPriority_Type = 3 +) + +var AccessPriority_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "LOW", + 2: "MEDIUM", + 3: "HIGH", +} +var AccessPriority_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "LOW": 1, + "MEDIUM": 2, + "HIGH": 3, +} + +func (x AccessPriority_Type) String() string { + return proto.EnumName(AccessPriority_Type_name, int32(x)) +} +func (AccessPriority_Type) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{0, 0} +} + +// Type of status +type AccessStatus_Type int32 + +const ( + // Unspecified, do NOT use + AccessStatus_TYPE_UNSPECIFIED AccessStatus_Type = 0 + // Enqueued for access to the resource + AccessStatus_QUEUED AccessStatus_Type = 1 + // Lease acquired for the resource + AccessStatus_LEASED AccessStatus_Type = 2 +) + +var AccessStatus_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "QUEUED", + 2: "LEASED", +} +var AccessStatus_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "QUEUED": 1, + "LEASED": 2, +} + +func (x AccessStatus_Type) String() string { + return proto.EnumName(AccessStatus_Type_name, int32(x)) +} +func (AccessStatus_Type) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{1, 0} +} + +// AccessPriority specifies the priority of a client's access to a semaphore resource +type AccessPriority struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AccessPriority) Reset() { *m = AccessPriority{} } +func (m *AccessPriority) String() string { return proto.CompactTextString(m) } +func (*AccessPriority) ProtoMessage() {} +func (*AccessPriority) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{0} +} +func (m *AccessPriority) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AccessPriority.Unmarshal(m, b) +} +func (m *AccessPriority) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AccessPriority.Marshal(b, m, deterministic) +} +func (dst *AccessPriority) XXX_Merge(src proto.Message) { + xxx_messageInfo_AccessPriority.Merge(dst, src) +} +func (m *AccessPriority) XXX_Size() int { + return xxx_messageInfo_AccessPriority.Size(m) +} +func (m *AccessPriority) XXX_DiscardUnknown() { + xxx_messageInfo_AccessPriority.DiscardUnknown(m) +} + +var xxx_messageInfo_AccessPriority proto.InternalMessageInfo + +// AccessStatus specifies the status of a client's access to a semaphore resource +type AccessStatus struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AccessStatus) Reset() { *m = AccessStatus{} } +func (m *AccessStatus) String() string { return proto.CompactTextString(m) } +func (*AccessStatus) ProtoMessage() {} +func (*AccessStatus) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{1} +} +func (m *AccessStatus) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AccessStatus.Unmarshal(m, b) +} +func (m *AccessStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AccessStatus.Marshal(b, m, deterministic) +} +func (dst *AccessStatus) XXX_Merge(src proto.Message) { + xxx_messageInfo_AccessStatus.Merge(dst, src) +} +func (m *AccessStatus) XXX_Size() int { + return xxx_messageInfo_AccessStatus.Size(m) +} +func (m *AccessStatus) XXX_DiscardUnknown() { + xxx_messageInfo_AccessStatus.DiscardUnknown(m) +} + +var xxx_messageInfo_AccessStatus proto.InternalMessageInfo + +// CreateRequest is the request to create a new semaphore resource +// +// resource_id and n_permits are required fields +// the other fields are optional and if skipped will be set to default values +type CreateRequest struct { + // Resource Id of the new semaphore resource + ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId" json:"resource_id,omitempty"` + // Number of permits that can be leased out for a resource + NPermits uint32 `protobuf:"varint,2,opt,name=n_permits,json=nPermits" json:"n_permits,omitempty"` + // Max duration for which a lease can be held + LeaseTimeout uint64 `protobuf:"varint,3,opt,name=lease_timeout,json=leaseTimeout" json:"lease_timeout,omitempty"` + // Max duration after which a client is considered dead if there is no heartbeat + DeadNodeTimeout uint64 `protobuf:"varint,4,opt,name=dead_node_timeout,json=deadNodeTimeout" json:"dead_node_timeout,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateRequest) Reset() { *m = CreateRequest{} } +func (m *CreateRequest) String() string { return proto.CompactTextString(m) } +func (*CreateRequest) ProtoMessage() {} +func (*CreateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{2} +} +func (m *CreateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateRequest.Unmarshal(m, b) +} +func (m *CreateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateRequest.Marshal(b, m, deterministic) +} +func (dst *CreateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateRequest.Merge(dst, src) +} +func (m *CreateRequest) XXX_Size() int { + return xxx_messageInfo_CreateRequest.Size(m) +} +func (m *CreateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CreateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateRequest proto.InternalMessageInfo + +func (m *CreateRequest) GetResourceId() string { + if m != nil { + return m.ResourceId + } + return "" +} + +func (m *CreateRequest) GetNPermits() uint32 { + if m != nil { + return m.NPermits + } + return 0 +} + +func (m *CreateRequest) GetLeaseTimeout() uint64 { + if m != nil { + return m.LeaseTimeout + } + return 0 +} + +func (m *CreateRequest) GetDeadNodeTimeout() uint64 { + if m != nil { + return m.DeadNodeTimeout + } + return 0 +} + +// CreateResponse is the response to create a new semaphore resource +type CreateResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateResponse) Reset() { *m = CreateResponse{} } +func (m *CreateResponse) String() string { return proto.CompactTextString(m) } +func (*CreateResponse) ProtoMessage() {} +func (*CreateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{3} +} +func (m *CreateResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateResponse.Unmarshal(m, b) +} +func (m *CreateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateResponse.Marshal(b, m, deterministic) +} +func (dst *CreateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateResponse.Merge(dst, src) +} +func (m *CreateResponse) XXX_Size() int { + return xxx_messageInfo_CreateResponse.Size(m) +} +func (m *CreateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CreateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateResponse proto.InternalMessageInfo + +// AcquireRequest is the request to acquire a lease for a semaphore resource +type AcquireRequest struct { + // Resource Id to acquire the lease for + // It should be the same as the resource_id in the CreateRequest + ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId" json:"resource_id,omitempty"` + // Client Id who is acquiring the lease + // client_id can be any unique identifier for the client + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId" json:"client_id,omitempty"` + // Priority of the client's access to the resource + AccessPriority AccessPriority_Type `protobuf:"varint,3,opt,name=access_priority,json=accessPriority,enum=operator.resourcegateway.AccessPriority_Type" json:"access_priority,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AcquireRequest) Reset() { *m = AcquireRequest{} } +func (m *AcquireRequest) String() string { return proto.CompactTextString(m) } +func (*AcquireRequest) ProtoMessage() {} +func (*AcquireRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{4} +} +func (m *AcquireRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AcquireRequest.Unmarshal(m, b) +} +func (m *AcquireRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AcquireRequest.Marshal(b, m, deterministic) +} +func (dst *AcquireRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_AcquireRequest.Merge(dst, src) +} +func (m *AcquireRequest) XXX_Size() int { + return xxx_messageInfo_AcquireRequest.Size(m) +} +func (m *AcquireRequest) XXX_DiscardUnknown() { + xxx_messageInfo_AcquireRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_AcquireRequest proto.InternalMessageInfo + +func (m *AcquireRequest) GetResourceId() string { + if m != nil { + return m.ResourceId + } + return "" +} + +func (m *AcquireRequest) GetClientId() string { + if m != nil { + return m.ClientId + } + return "" +} + +func (m *AcquireRequest) GetAccessPriority() AccessPriority_Type { + if m != nil { + return m.AccessPriority + } + return AccessPriority_TYPE_UNSPECIFIED +} + +// AcquireResponse is the response to acquire a semaphore lock +type AcquireResponse struct { + // Status of the client's access to the resource + AccessStatus AccessStatus_Type `protobuf:"varint,1,opt,name=access_status,json=accessStatus,enum=operator.resourcegateway.AccessStatus_Type" json:"access_status,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AcquireResponse) Reset() { *m = AcquireResponse{} } +func (m *AcquireResponse) String() string { return proto.CompactTextString(m) } +func (*AcquireResponse) ProtoMessage() {} +func (*AcquireResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{5} +} +func (m *AcquireResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AcquireResponse.Unmarshal(m, b) +} +func (m *AcquireResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AcquireResponse.Marshal(b, m, deterministic) +} +func (dst *AcquireResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_AcquireResponse.Merge(dst, src) +} +func (m *AcquireResponse) XXX_Size() int { + return xxx_messageInfo_AcquireResponse.Size(m) +} +func (m *AcquireResponse) XXX_DiscardUnknown() { + xxx_messageInfo_AcquireResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_AcquireResponse proto.InternalMessageInfo + +func (m *AcquireResponse) GetAccessStatus() AccessStatus_Type { + if m != nil { + return m.AccessStatus + } + return AccessStatus_TYPE_UNSPECIFIED +} + +// ReleaseRequest is the request to release a semaphore lock +type ReleaseRequest struct { + // Resource Id to release the lease for + ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId" json:"resource_id,omitempty"` + // Client Id who is releasing the lease + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId" json:"client_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReleaseRequest) Reset() { *m = ReleaseRequest{} } +func (m *ReleaseRequest) String() string { return proto.CompactTextString(m) } +func (*ReleaseRequest) ProtoMessage() {} +func (*ReleaseRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{6} +} +func (m *ReleaseRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReleaseRequest.Unmarshal(m, b) +} +func (m *ReleaseRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReleaseRequest.Marshal(b, m, deterministic) +} +func (dst *ReleaseRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReleaseRequest.Merge(dst, src) +} +func (m *ReleaseRequest) XXX_Size() int { + return xxx_messageInfo_ReleaseRequest.Size(m) +} +func (m *ReleaseRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ReleaseRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ReleaseRequest proto.InternalMessageInfo + +func (m *ReleaseRequest) GetResourceId() string { + if m != nil { + return m.ResourceId + } + return "" +} + +func (m *ReleaseRequest) GetClientId() string { + if m != nil { + return m.ClientId + } + return "" +} + +// ReleaseResponse is the response to release a semaphore lock +type ReleaseResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReleaseResponse) Reset() { *m = ReleaseResponse{} } +func (m *ReleaseResponse) String() string { return proto.CompactTextString(m) } +func (*ReleaseResponse) ProtoMessage() {} +func (*ReleaseResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{7} +} +func (m *ReleaseResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReleaseResponse.Unmarshal(m, b) +} +func (m *ReleaseResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReleaseResponse.Marshal(b, m, deterministic) +} +func (dst *ReleaseResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReleaseResponse.Merge(dst, src) +} +func (m *ReleaseResponse) XXX_Size() int { + return xxx_messageInfo_ReleaseResponse.Size(m) +} +func (m *ReleaseResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ReleaseResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ReleaseResponse proto.InternalMessageInfo + +// HeartbeatRequest is the request to send a heartbeat to the semaphore service +type HeartbeatRequest struct { + // Resource ID to keep the lease alive for + ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId" json:"resource_id,omitempty"` + // Client ID to keep the lease alive for + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId" json:"client_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HeartbeatRequest) Reset() { *m = HeartbeatRequest{} } +func (m *HeartbeatRequest) String() string { return proto.CompactTextString(m) } +func (*HeartbeatRequest) ProtoMessage() {} +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{8} +} +func (m *HeartbeatRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HeartbeatRequest.Unmarshal(m, b) +} +func (m *HeartbeatRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HeartbeatRequest.Marshal(b, m, deterministic) +} +func (dst *HeartbeatRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_HeartbeatRequest.Merge(dst, src) +} +func (m *HeartbeatRequest) XXX_Size() int { + return xxx_messageInfo_HeartbeatRequest.Size(m) +} +func (m *HeartbeatRequest) XXX_DiscardUnknown() { + xxx_messageInfo_HeartbeatRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_HeartbeatRequest proto.InternalMessageInfo + +func (m *HeartbeatRequest) GetResourceId() string { + if m != nil { + return m.ResourceId + } + return "" +} + +func (m *HeartbeatRequest) GetClientId() string { + if m != nil { + return m.ClientId + } + return "" +} + +// HeartbeatLockResponse is the response to keep a semaphore lock alive +type HeartbeatResponse struct { + // Status of the client's access to the resource + AccessStatus AccessStatus_Type `protobuf:"varint,1,opt,name=access_status,json=accessStatus,enum=operator.resourcegateway.AccessStatus_Type" json:"access_status,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HeartbeatResponse) Reset() { *m = HeartbeatResponse{} } +func (m *HeartbeatResponse) String() string { return proto.CompactTextString(m) } +func (*HeartbeatResponse) ProtoMessage() {} +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_resource_gateway_1e8a27e27414e1c7, []int{9} +} +func (m *HeartbeatResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HeartbeatResponse.Unmarshal(m, b) +} +func (m *HeartbeatResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HeartbeatResponse.Marshal(b, m, deterministic) +} +func (dst *HeartbeatResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_HeartbeatResponse.Merge(dst, src) +} +func (m *HeartbeatResponse) XXX_Size() int { + return xxx_messageInfo_HeartbeatResponse.Size(m) +} +func (m *HeartbeatResponse) XXX_DiscardUnknown() { + xxx_messageInfo_HeartbeatResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_HeartbeatResponse proto.InternalMessageInfo + +func (m *HeartbeatResponse) GetAccessStatus() AccessStatus_Type { + if m != nil { + return m.AccessStatus + } + return AccessStatus_TYPE_UNSPECIFIED +} + +func init() { + proto.RegisterType((*AccessPriority)(nil), "operator.resourcegateway.AccessPriority") + proto.RegisterType((*AccessStatus)(nil), "operator.resourcegateway.AccessStatus") + proto.RegisterType((*CreateRequest)(nil), "operator.resourcegateway.CreateRequest") + proto.RegisterType((*CreateResponse)(nil), "operator.resourcegateway.CreateResponse") + proto.RegisterType((*AcquireRequest)(nil), "operator.resourcegateway.AcquireRequest") + proto.RegisterType((*AcquireResponse)(nil), "operator.resourcegateway.AcquireResponse") + proto.RegisterType((*ReleaseRequest)(nil), "operator.resourcegateway.ReleaseRequest") + proto.RegisterType((*ReleaseResponse)(nil), "operator.resourcegateway.ReleaseResponse") + proto.RegisterType((*HeartbeatRequest)(nil), "operator.resourcegateway.HeartbeatRequest") + proto.RegisterType((*HeartbeatResponse)(nil), "operator.resourcegateway.HeartbeatResponse") + proto.RegisterEnum("operator.resourcegateway.AccessPriority_Type", AccessPriority_Type_name, AccessPriority_Type_value) + proto.RegisterEnum("operator.resourcegateway.AccessStatus_Type", AccessStatus_Type_name, AccessStatus_Type_value) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for SemaphoreService service + +type SemaphoreServiceClient interface { + // Create creates a new semaphore resource + // It initializes a semaphore for the resource in memory and persists it in the backend + // + // resource_id and n_permits are required fields + // the other fields are optional and if skipped will be set to default values + // + // The first request every client makes to the semaphore service should be a create request + // + // Only the first request received by the semaphore service will create the resource + // Any subsequent requests will be ignored + Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) + // Acquire acquires a lease for the resource or reserves a place in the queue + // + // Clients will poll this endpoint to acquire a lease for the resource + // On the first poll, they will either be granted a lease or enqueued + // If enqueued, the client should poll periodically to check if the lease is granted + // + // The client is enqueued in the priority queue corresponding to the access_priority + // Leases are granted according to priority that is, + // first all high priority clients are granted leases followed by the others in order + // + // A client can be starved by other clients with higher priority + // Consider implementing a client-side timeout for how long to wait for the lease + // before bypassing the semaphore access (if possible) + // The priority can not be bumped up once the client is enqueued (Future work) + // + // No need to send a heartbeat while the client is enqueued + // Heartbeats are updated implicitly on every Acquire request + Acquire(ctx context.Context, in *AcquireRequest, opts ...grpc.CallOption) (*AcquireResponse, error) + // Release releases the lease on the resource + // + // If the lease has expired, the release request will be a noop + Release(ctx context.Context, in *ReleaseRequest, opts ...grpc.CallOption) (*ReleaseResponse, error) + // Heartbeat keeps the lease alive for the resource + // + // Clients should periodically send heartbeats once they have acquired the lease + // If no heartbeat is received within the dead_node_timeout, the lease will be revoked + // + // Client should monitor the access status returned to check if the lease is still valid + // if the lease is lost the client should take the necessary action + // + // + Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) +} + +type semaphoreServiceClient struct { + cc *grpc.ClientConn +} + +func NewSemaphoreServiceClient(cc *grpc.ClientConn) SemaphoreServiceClient { + return &semaphoreServiceClient{cc} +} + +func (c *semaphoreServiceClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) { + out := new(CreateResponse) + err := grpc.Invoke(ctx, "/operator.resourcegateway.SemaphoreService/Create", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *semaphoreServiceClient) Acquire(ctx context.Context, in *AcquireRequest, opts ...grpc.CallOption) (*AcquireResponse, error) { + out := new(AcquireResponse) + err := grpc.Invoke(ctx, "/operator.resourcegateway.SemaphoreService/Acquire", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *semaphoreServiceClient) Release(ctx context.Context, in *ReleaseRequest, opts ...grpc.CallOption) (*ReleaseResponse, error) { + out := new(ReleaseResponse) + err := grpc.Invoke(ctx, "/operator.resourcegateway.SemaphoreService/Release", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *semaphoreServiceClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) { + out := new(HeartbeatResponse) + err := grpc.Invoke(ctx, "/operator.resourcegateway.SemaphoreService/Heartbeat", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for SemaphoreService service + +type SemaphoreServiceServer interface { + // Create creates a new semaphore resource + // It initializes a semaphore for the resource in memory and persists it in the backend + // + // resource_id and n_permits are required fields + // the other fields are optional and if skipped will be set to default values + // + // The first request every client makes to the semaphore service should be a create request + // + // Only the first request received by the semaphore service will create the resource + // Any subsequent requests will be ignored + Create(context.Context, *CreateRequest) (*CreateResponse, error) + // Acquire acquires a lease for the resource or reserves a place in the queue + // + // Clients will poll this endpoint to acquire a lease for the resource + // On the first poll, they will either be granted a lease or enqueued + // If enqueued, the client should poll periodically to check if the lease is granted + // + // The client is enqueued in the priority queue corresponding to the access_priority + // Leases are granted according to priority that is, + // first all high priority clients are granted leases followed by the others in order + // + // A client can be starved by other clients with higher priority + // Consider implementing a client-side timeout for how long to wait for the lease + // before bypassing the semaphore access (if possible) + // The priority can not be bumped up once the client is enqueued (Future work) + // + // No need to send a heartbeat while the client is enqueued + // Heartbeats are updated implicitly on every Acquire request + Acquire(context.Context, *AcquireRequest) (*AcquireResponse, error) + // Release releases the lease on the resource + // + // If the lease has expired, the release request will be a noop + Release(context.Context, *ReleaseRequest) (*ReleaseResponse, error) + // Heartbeat keeps the lease alive for the resource + // + // Clients should periodically send heartbeats once they have acquired the lease + // If no heartbeat is received within the dead_node_timeout, the lease will be revoked + // + // Client should monitor the access status returned to check if the lease is still valid + // if the lease is lost the client should take the necessary action + // + // + Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) +} + +func RegisterSemaphoreServiceServer(s *grpc.Server, srv SemaphoreServiceServer) { + s.RegisterService(&_SemaphoreService_serviceDesc, srv) +} + +func _SemaphoreService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SemaphoreServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/operator.resourcegateway.SemaphoreService/Create", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SemaphoreServiceServer).Create(ctx, req.(*CreateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SemaphoreService_Acquire_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AcquireRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SemaphoreServiceServer).Acquire(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/operator.resourcegateway.SemaphoreService/Acquire", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SemaphoreServiceServer).Acquire(ctx, req.(*AcquireRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SemaphoreService_Release_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SemaphoreServiceServer).Release(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/operator.resourcegateway.SemaphoreService/Release", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SemaphoreServiceServer).Release(ctx, req.(*ReleaseRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SemaphoreService_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HeartbeatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SemaphoreServiceServer).Heartbeat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/operator.resourcegateway.SemaphoreService/Heartbeat", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SemaphoreServiceServer).Heartbeat(ctx, req.(*HeartbeatRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _SemaphoreService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "operator.resourcegateway.SemaphoreService", + HandlerType: (*SemaphoreServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Create", + Handler: _SemaphoreService_Create_Handler, + }, + { + MethodName: "Acquire", + Handler: _SemaphoreService_Acquire_Handler, + }, + { + MethodName: "Release", + Handler: _SemaphoreService_Release_Handler, + }, + { + MethodName: "Heartbeat", + Handler: _SemaphoreService_Heartbeat_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "resource_gateway.proto", +} + +func init() { + proto.RegisterFile("resource_gateway.proto", fileDescriptor_resource_gateway_1e8a27e27414e1c7) +} + +var fileDescriptor_resource_gateway_1e8a27e27414e1c7 = []byte{ + // 535 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x54, 0x51, 0x8f, 0xd2, 0x4c, + 0x14, 0xa5, 0x40, 0x58, 0xb8, 0x1f, 0x94, 0x32, 0xd9, 0x7c, 0x21, 0x68, 0x22, 0xa9, 0x0f, 0xe2, + 0x6e, 0xc4, 0x04, 0x7d, 0xdb, 0xa7, 0xdd, 0xa5, 0x2e, 0x4d, 0x16, 0xac, 0x05, 0x34, 0x9a, 0x98, + 0x3a, 0xdb, 0x5e, 0xb5, 0xc9, 0xc2, 0x74, 0x67, 0x06, 0x0d, 0x3f, 0x46, 0x1f, 0xfd, 0x9d, 0xa6, + 0x9d, 0x16, 0x81, 0x04, 0x76, 0x4d, 0xd6, 0xc7, 0xde, 0x7b, 0xe6, 0x9e, 0xd3, 0x73, 0xee, 0x0c, + 0xfc, 0xcf, 0x51, 0xb0, 0x05, 0xf7, 0xd1, 0xfb, 0x42, 0x25, 0x7e, 0xa7, 0xcb, 0x6e, 0xc4, 0x99, + 0x64, 0xa4, 0xc9, 0x22, 0xe4, 0x54, 0x32, 0xde, 0xcd, 0x00, 0x69, 0xdf, 0x1c, 0x82, 0x7e, 0xea, + 0xfb, 0x28, 0x84, 0xc3, 0x43, 0xc6, 0x43, 0xb9, 0x34, 0x4f, 0xa0, 0x38, 0x59, 0x46, 0x48, 0x0e, + 0xc1, 0x98, 0xbc, 0x77, 0x2c, 0x6f, 0x3a, 0x1a, 0x3b, 0xd6, 0xb9, 0xfd, 0xca, 0xb6, 0xfa, 0x46, + 0x8e, 0x1c, 0x40, 0xe1, 0xf2, 0xf5, 0x3b, 0x43, 0x23, 0x00, 0xa5, 0xa1, 0xd5, 0xb7, 0xa7, 0x43, + 0x23, 0x4f, 0xca, 0x50, 0x1c, 0xd8, 0x17, 0x03, 0xa3, 0x60, 0xf6, 0xa1, 0xaa, 0xc6, 0x8d, 0x25, + 0x95, 0x0b, 0x61, 0xbe, 0xdc, 0x3b, 0x0c, 0xa0, 0xf4, 0x66, 0x6a, 0x4d, 0xad, 0xbe, 0x9a, 0x77, + 0x69, 0x9d, 0x8e, 0xad, 0xbe, 0x91, 0x37, 0x7f, 0x6a, 0x50, 0x3b, 0xe7, 0x48, 0x25, 0xba, 0x78, + 0xb3, 0x40, 0x21, 0xc9, 0x23, 0xf8, 0x6f, 0xf5, 0x6b, 0x61, 0xd0, 0xd4, 0xda, 0x5a, 0xa7, 0xe2, + 0x42, 0x56, 0xb2, 0x03, 0xf2, 0x00, 0x2a, 0x73, 0x2f, 0x42, 0x3e, 0x0b, 0xa5, 0x68, 0xe6, 0xdb, + 0x5a, 0xa7, 0xe6, 0x96, 0xe7, 0x8e, 0xfa, 0x26, 0x8f, 0xa1, 0x76, 0x8d, 0x54, 0xa0, 0x27, 0xc3, + 0x19, 0xb2, 0x85, 0x6c, 0x16, 0xda, 0x5a, 0xa7, 0xe8, 0x56, 0x93, 0xe2, 0x44, 0xd5, 0xc8, 0x11, + 0x34, 0x02, 0xa4, 0x81, 0x37, 0x67, 0xc1, 0x1f, 0x60, 0x31, 0x01, 0xd6, 0xe3, 0xc6, 0x88, 0x05, + 0x19, 0xd6, 0x34, 0x40, 0xcf, 0xf4, 0x89, 0x88, 0xcd, 0x05, 0x9a, 0xbf, 0xb4, 0xd8, 0xc8, 0x9b, + 0x45, 0xc8, 0xff, 0x4a, 0xb3, 0x7f, 0x1d, 0xe2, 0x5c, 0xc6, 0xed, 0x7c, 0xd2, 0x2e, 0xab, 0x82, + 0x1d, 0x90, 0xb7, 0x50, 0xa7, 0x89, 0x93, 0x5e, 0x94, 0x26, 0x93, 0xa8, 0xd6, 0x7b, 0xcf, 0xba, + 0xbb, 0xc2, 0xec, 0x6e, 0x26, 0xd9, 0x8d, 0x9d, 0x77, 0x75, 0xba, 0x19, 0xaf, 0x0f, 0xf5, 0x95, + 0x4e, 0xa5, 0x9d, 0x38, 0x50, 0x4b, 0xa9, 0x44, 0x92, 0x5a, 0x22, 0x55, 0xef, 0x1d, 0xdf, 0x46, + 0xa4, 0x32, 0x56, 0x34, 0x55, 0xba, 0x1e, 0xfb, 0x08, 0x74, 0x17, 0x13, 0x77, 0xef, 0xc5, 0x0c, + 0xb3, 0x01, 0xf5, 0xd5, 0xbc, 0xd4, 0x70, 0x07, 0x8c, 0x01, 0x52, 0x2e, 0xaf, 0x90, 0xca, 0xfb, + 0x21, 0x41, 0x68, 0xac, 0x4d, 0xfc, 0x57, 0xde, 0xf4, 0x7e, 0x14, 0xc0, 0x18, 0xe3, 0x8c, 0x46, + 0x5f, 0x19, 0xc7, 0x31, 0xf2, 0x6f, 0xa1, 0x8f, 0xe4, 0x23, 0x94, 0xd4, 0x42, 0x91, 0x27, 0xbb, + 0x27, 0x6f, 0x5c, 0x89, 0x56, 0xe7, 0x76, 0x60, 0x6a, 0x55, 0x8e, 0x7c, 0x82, 0x83, 0x34, 0x74, + 0xd2, 0xd9, 0xa7, 0x7c, 0x7d, 0x7f, 0x5b, 0x4f, 0xef, 0x80, 0x5c, 0x67, 0x48, 0x13, 0xda, 0xc7, + 0xb0, 0xb9, 0x14, 0xfb, 0x18, 0xb6, 0xe3, 0xce, 0x91, 0xcf, 0x50, 0x59, 0xc5, 0x43, 0x8e, 0x76, + 0x9f, 0xdc, 0xde, 0x8a, 0xd6, 0xf1, 0x9d, 0xb0, 0x19, 0xcf, 0x99, 0x0d, 0x0f, 0x7d, 0x36, 0xdb, + 0x79, 0xe6, 0xec, 0xd0, 0x4d, 0x0b, 0x17, 0xaa, 0xe0, 0xc4, 0x2f, 0xac, 0xa3, 0x7d, 0x68, 0x3c, + 0x3f, 0xd9, 0x82, 0x5e, 0x95, 0x92, 0xd7, 0xf7, 0xc5, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x30, + 0x19, 0x29, 0x38, 0x97, 0x05, 0x00, 0x00, +} diff --git a/proto/resource_gateway.proto b/proto/resource_gateway.proto new file mode 100644 index 0000000000..2aedfff44f --- /dev/null +++ b/proto/resource_gateway.proto @@ -0,0 +1,152 @@ +syntax = "proto3"; + +package operator.resourcegateway; + +option go_package = "/;resourcegateway"; +option java_multiple_files = true; +option java_package = "com.operator.resourcegateway"; +option java_outer_classname = "ResourceGatewayProto"; + +// SemaphoreService provides a counting semaphore to limit access to resources +// and a priority queue to distinguish between different types of clients/requests +service SemaphoreService { + // Create creates a new semaphore resource + // It initializes a semaphore for the resource in memory and persists it in the backend + // + // resource_id and n_permits are required fields + // the other fields are optional and if skipped will be set to default values + // + // The first request every client makes to the semaphore service should be a create request + // + // Only the first request received by the semaphore service will create the resource + // Any subsequent requests will be ignored + rpc Create(CreateRequest) + returns (CreateResponse) {} + + // Acquire acquires a lease for the resource or reserves a place in the queue + // + // Clients will poll this endpoint to acquire a lease for the resource + // On the first poll, they will either be granted a lease or enqueued + // If enqueued, the client should poll periodically to check if the lease is granted + // + // The client is enqueued in the priority queue corresponding to the access_priority + // Leases are granted according to priority that is, + // first all high priority clients are granted leases followed by the others in order + // + // A client can be starved by other clients with higher priority + // Consider implementing a client-side timeout for how long to wait for the lease + // before bypassing the semaphore access (if possible) + // The priority can not be bumped up once the client is enqueued (Future work) + // + // No need to send a heartbeat while the client is enqueued + // Heartbeats are updated implicitly on every Acquire request + rpc Acquire(AcquireRequest) + returns (AcquireResponse) {} + + // Release releases the lease on the resource + // + // If the lease has expired, the release request will be a noop +rpc Release (ReleaseRequest) + returns (ReleaseResponse) {} + + // Heartbeat keeps the lease alive for the resource + // + // Clients should periodically send heartbeats once they have acquired the lease + // If no heartbeat is received within the dead_node_timeout, the lease will be revoked + // + // Client should monitor the access status returned to check if the lease is still valid + // if the lease is lost the client should take the necessary action + // + // + rpc Heartbeat (HeartbeatRequest) + returns (HeartbeatResponse) {} +} + +// AccessPriority specifies the priority of a client's access to a semaphore resource +message AccessPriority { + // Type of priority + enum Type { + // Unspecified, do NOT use + TYPE_UNSPECIFIED = 0; + // Enqueued to low priority queue + LOW = 1; + // Enqueued to medium priority queue + MEDIUM = 2; + // Enqueued to high priority queue + HIGH = 3; + } +} + +// AccessStatus specifies the status of a client's access to a semaphore resource +message AccessStatus{ + // Type of status + enum Type { + // Unspecified, do NOT use + TYPE_UNSPECIFIED = 0; + // Enqueued for access to the resource + QUEUED = 1; + // Lease acquired for the resource + LEASED = 2; + } +} + +// CreateRequest is the request to create a new semaphore resource +// +// resource_id and n_permits are required fields +// the other fields are optional and if skipped will be set to default values +message CreateRequest { + // Resource Id of the new semaphore resource + string resource_id = 1; + // Number of permits that can be leased out for a resource + uint32 n_permits = 2; + // Max duration for which a lease can be held + uint64 lease_timeout = 3; + // Max duration after which a client is considered dead if there is no heartbeat + uint64 dead_node_timeout = 4; +} + +// CreateResponse is the response to create a new semaphore resource +message CreateResponse {} + +// AcquireRequest is the request to acquire a lease for a semaphore resource +message AcquireRequest { + // Resource Id to acquire the lease for + // It should be the same as the resource_id in the CreateRequest + string resource_id = 1; + // Client Id who is acquiring the lease + // client_id can be any unique identifier for the client + string client_id = 2; + // Priority of the client's access to the resource + AccessPriority.Type access_priority = 3; +} + +// AcquireResponse is the response to acquire a semaphore lock +message AcquireResponse { + // Status of the client's access to the resource + AccessStatus.Type access_status = 1; +} + +// ReleaseRequest is the request to release a semaphore lock +message ReleaseRequest { + // Resource Id to release the lease for + string resource_id = 1; + // Client Id who is releasing the lease + string client_id = 2; +} + +// ReleaseResponse is the response to release a semaphore lock +message ReleaseResponse {} + +// HeartbeatRequest is the request to send a heartbeat to the semaphore service +message HeartbeatRequest { + // Resource ID to keep the lease alive for + string resource_id = 1; + // Client ID to keep the lease alive for + string client_id = 2; +} + +// HeartbeatLockResponse is the response to keep a semaphore lock alive +message HeartbeatResponse { + // Status of the client's access to the resource + AccessStatus.Type access_status = 1; +} diff --git a/vendor/github.com/cespare/xxhash/v2/README.md b/vendor/github.com/cespare/xxhash/v2/README.md index 8bf0e5b781..33c88305c4 100644 --- a/vendor/github.com/cespare/xxhash/v2/README.md +++ b/vendor/github.com/cespare/xxhash/v2/README.md @@ -70,3 +70,5 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$') - [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) - [FreeCache](https://github.com/coocood/freecache) - [FastCache](https://github.com/VictoriaMetrics/fastcache) +- [Ristretto](https://github.com/dgraph-io/ristretto) +- [Badger](https://github.com/dgraph-io/badger) diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash.go b/vendor/github.com/cespare/xxhash/v2/xxhash.go index a9e0d45c9d..78bddf1cee 100644 --- a/vendor/github.com/cespare/xxhash/v2/xxhash.go +++ b/vendor/github.com/cespare/xxhash/v2/xxhash.go @@ -19,10 +19,13 @@ const ( // Store the primes in an array as well. // // The consts are used when possible in Go code to avoid MOVs but we need a -// contiguous array of the assembly code. +// contiguous array for the assembly code. var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5} // Digest implements hash.Hash64. +// +// Note that a zero-valued Digest is not ready to receive writes. +// Call Reset or create a Digest using New before calling other methods. type Digest struct { v1 uint64 v2 uint64 @@ -33,19 +36,31 @@ type Digest struct { n int // how much of mem is used } -// New creates a new Digest that computes the 64-bit xxHash algorithm. +// New creates a new Digest with a zero seed. func New() *Digest { + return NewWithSeed(0) +} + +// NewWithSeed creates a new Digest with the given seed. +func NewWithSeed(seed uint64) *Digest { var d Digest - d.Reset() + d.ResetWithSeed(seed) return &d } // Reset clears the Digest's state so that it can be reused. +// It uses a seed value of zero. func (d *Digest) Reset() { - d.v1 = primes[0] + prime2 - d.v2 = prime2 - d.v3 = 0 - d.v4 = -primes[0] + d.ResetWithSeed(0) +} + +// ResetWithSeed clears the Digest's state so that it can be reused. +// It uses the given seed to initialize the state. +func (d *Digest) ResetWithSeed(seed uint64) { + d.v1 = seed + prime1 + prime2 + d.v2 = seed + prime2 + d.v3 = seed + d.v4 = seed - prime1 d.total = 0 d.n = 0 } diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go b/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go index 9216e0a40c..78f95f2561 100644 --- a/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go @@ -6,7 +6,7 @@ package xxhash -// Sum64 computes the 64-bit xxHash digest of b. +// Sum64 computes the 64-bit xxHash digest of b with a zero seed. // //go:noescape func Sum64(b []byte) uint64 diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_other.go b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go index 26df13bba4..118e49e819 100644 --- a/vendor/github.com/cespare/xxhash/v2/xxhash_other.go +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go @@ -3,7 +3,7 @@ package xxhash -// Sum64 computes the 64-bit xxHash digest of b. +// Sum64 computes the 64-bit xxHash digest of b with a zero seed. func Sum64(b []byte) uint64 { // A simpler version would be // d := New() diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go index e86f1b5fd8..05f5e7dfe7 100644 --- a/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go @@ -5,7 +5,7 @@ package xxhash -// Sum64String computes the 64-bit xxHash digest of s. +// Sum64String computes the 64-bit xxHash digest of s with a zero seed. func Sum64String(s string) uint64 { return Sum64([]byte(s)) } diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go index 1c1638fd88..cf9d42aed5 100644 --- a/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go @@ -33,7 +33,7 @@ import ( // // See https://github.com/golang/go/issues/42739 for discussion. -// Sum64String computes the 64-bit xxHash digest of s. +// Sum64String computes the 64-bit xxHash digest of s with a zero seed. // It may be faster than Sum64([]byte(s)) by avoiding a copy. func Sum64String(s string) uint64 { b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) diff --git a/vendor/github.com/matttproud/golang_protobuf_extensions/LICENSE b/vendor/github.com/coreos/go-oidc/v3/LICENSE similarity index 99% rename from vendor/github.com/matttproud/golang_protobuf_extensions/LICENSE rename to vendor/github.com/coreos/go-oidc/v3/LICENSE index 8dada3edaf..e06d208186 100644 --- a/vendor/github.com/matttproud/golang_protobuf_extensions/LICENSE +++ b/vendor/github.com/coreos/go-oidc/v3/LICENSE @@ -1,4 +1,4 @@ - Apache License +Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -199,3 +199,4 @@ 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. + diff --git a/vendor/github.com/coreos/go-oidc/v3/NOTICE b/vendor/github.com/coreos/go-oidc/v3/NOTICE new file mode 100644 index 0000000000..b39ddfa5cb --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/NOTICE @@ -0,0 +1,5 @@ +CoreOS Project +Copyright 2014 CoreOS, Inc + +This product includes software developed at CoreOS, Inc. +(http://www.coreos.com/). diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go new file mode 100644 index 0000000000..f42d37d481 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go @@ -0,0 +1,32 @@ +package oidc + +import jose "github.com/go-jose/go-jose/v4" + +// JOSE asymmetric signing algorithm values as defined by RFC 7518 +// +// see: https://tools.ietf.org/html/rfc7518#section-3.1 +const ( + RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256 + RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384 + RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512 + ES256 = "ES256" // ECDSA using P-256 and SHA-256 + ES384 = "ES384" // ECDSA using P-384 and SHA-384 + ES512 = "ES512" // ECDSA using P-521 and SHA-512 + PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256 + PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384 + PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512 + EdDSA = "EdDSA" // Ed25519 using SHA-512 +) + +var allAlgs = []jose.SignatureAlgorithm{ + jose.RS256, + jose.RS384, + jose.RS512, + jose.ES256, + jose.ES384, + jose.ES512, + jose.PS256, + jose.PS384, + jose.PS512, + jose.EdDSA, +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go new file mode 100644 index 0000000000..6a846ece95 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go @@ -0,0 +1,269 @@ +package oidc + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + jose "github.com/go-jose/go-jose/v4" +) + +// StaticKeySet is a verifier that validates JWT against a static set of public keys. +type StaticKeySet struct { + // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and + // *ecdsa.PublicKey. + PublicKeys []crypto.PublicKey +} + +// VerifySignature compares the signature against a static set of public keys. +func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { + // Algorithms are already checked by Verifier, so this parse method accepts + // any algorithm. + jws, err := jose.ParseSigned(jwt, allAlgs) + if err != nil { + return nil, fmt.Errorf("parsing jwt: %v", err) + } + for _, pub := range s.PublicKeys { + switch pub.(type) { + case *rsa.PublicKey: + case *ecdsa.PublicKey: + case ed25519.PublicKey: + default: + return nil, fmt.Errorf("invalid public key type provided: %T", pub) + } + payload, err := jws.Verify(pub) + if err != nil { + continue + } + return payload, nil + } + return nil, fmt.Errorf("no public keys able to verify jwt") +} + +// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP +// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically +// used by NewProvider using the URLs returned by OpenID Connect discovery, but is +// exposed for providers that don't support discovery or to prevent round trips to the +// discovery URL. +// +// The returned KeySet is a long lived verifier that caches keys based on any +// keys change. Reuse a common remote key set instead of creating new ones as needed. +func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet { + return newRemoteKeySet(ctx, jwksURL, time.Now) +} + +func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet { + if now == nil { + now = time.Now + } + return &RemoteKeySet{ + jwksURL: jwksURL, + now: now, + // For historical reasons, this package uses contexts for configuration, not just + // cancellation. In hindsight, this was a bad idea. + // + // Attemps to reason about how cancels should work with background requests have + // largely lead to confusion. Use the context here as a config bag-of-values and + // ignore the cancel function. + ctx: context.WithoutCancel(ctx), + } +} + +// RemoteKeySet is a KeySet implementation that validates JSON web tokens against +// a jwks_uri endpoint. +type RemoteKeySet struct { + jwksURL string + now func() time.Time + + // Used for configuration. Cancelation is ignored. + ctx context.Context + + // guard all other fields + mu sync.RWMutex + + // inflight suppresses parallel execution of updateKeys and allows + // multiple goroutines to wait for its result. + inflight *inflight + + // A set of cached keys. + cachedKeys []jose.JSONWebKey +} + +// inflight is used to wait on some in-flight request from multiple goroutines. +type inflight struct { + doneCh chan struct{} + + keys []jose.JSONWebKey + err error +} + +func newInflight() *inflight { + return &inflight{doneCh: make(chan struct{})} +} + +// wait returns a channel that multiple goroutines can receive on. Once it returns +// a value, the inflight request is done and result() can be inspected. +func (i *inflight) wait() <-chan struct{} { + return i.doneCh +} + +// done can only be called by a single goroutine. It records the result of the +// inflight request and signals other goroutines that the result is safe to +// inspect. +func (i *inflight) done(keys []jose.JSONWebKey, err error) { + i.keys = keys + i.err = err + close(i.doneCh) +} + +// result cannot be called until the wait() channel has returned a value. +func (i *inflight) result() ([]jose.JSONWebKey, error) { + return i.keys, i.err +} + +// paresdJWTKey is a context key that allows common setups to avoid parsing the +// JWT twice. It holds a *jose.JSONWebSignature value. +var parsedJWTKey contextKey + +// VerifySignature validates a payload against a signature from the jwks_uri. +// +// Users MUST NOT call this method directly and should use an IDTokenVerifier +// instead. This method skips critical validations such as 'alg' values and is +// only exported to implement the KeySet interface. +func (r *RemoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { + jws, ok := ctx.Value(parsedJWTKey).(*jose.JSONWebSignature) + if !ok { + // The algorithm values are already enforced by the Validator, which also sets + // the context value above to pre-parsed signature. + // + // Practically, this codepath isn't called in normal use of this package, but + // if it is, the algorithms have already been checked. + var err error + jws, err = jose.ParseSigned(jwt, allAlgs) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %v", err) + } + } + return r.verify(ctx, jws) +} + +func (r *RemoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { + // We don't support JWTs signed with multiple signatures. + keyID := "" + for _, sig := range jws.Signatures { + keyID = sig.Header.KeyID + break + } + + keys := r.keysFromCache() + for _, key := range keys { + if keyID == "" || key.KeyID == keyID { + if payload, err := jws.Verify(&key); err == nil { + return payload, nil + } + } + } + + // If the kid doesn't match, check for new keys from the remote. This is the + // strategy recommended by the spec. + // + // https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys + keys, err := r.keysFromRemote(ctx) + if err != nil { + return nil, fmt.Errorf("fetching keys %w", err) + } + + for _, key := range keys { + if keyID == "" || key.KeyID == keyID { + if payload, err := jws.Verify(&key); err == nil { + return payload, nil + } + } + } + return nil, errors.New("failed to verify id token signature") +} + +func (r *RemoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { + r.mu.RLock() + defer r.mu.RUnlock() + return r.cachedKeys +} + +// keysFromRemote syncs the key set from the remote set, records the values in the +// cache, and returns the key set. +func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { + // Need to lock to inspect the inflight request field. + r.mu.Lock() + // If there's not a current inflight request, create one. + if r.inflight == nil { + r.inflight = newInflight() + + // This goroutine has exclusive ownership over the current inflight + // request. It releases the resource by nil'ing the inflight field + // once the goroutine is done. + go func() { + // Sync keys and finish inflight when that's done. + keys, err := r.updateKeys() + + r.inflight.done(keys, err) + + // Lock to update the keys and indicate that there is no longer an + // inflight request. + r.mu.Lock() + defer r.mu.Unlock() + + if err == nil { + r.cachedKeys = keys + } + + // Free inflight so a different request can run. + r.inflight = nil + }() + } + inflight := r.inflight + r.mu.Unlock() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-inflight.wait(): + return inflight.result() + } +} + +func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { + req, err := http.NewRequest("GET", r.jwksURL, nil) + if err != nil { + return nil, fmt.Errorf("oidc: can't create request: %v", err) + } + + resp, err := doRequest(r.ctx, req) + if err != nil { + return nil, fmt.Errorf("oidc: get keys failed %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body) + } + + var keySet jose.JSONWebKeySet + err = unmarshalResp(resp, body, &keySet) + if err != nil { + return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body) + } + return keySet.Keys, nil +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go new file mode 100644 index 0000000000..17419f3883 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go @@ -0,0 +1,554 @@ +// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. +package oidc + +import ( + "context" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "mime" + "net/http" + "strings" + "sync" + "time" + + "golang.org/x/oauth2" +) + +const ( + // ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests. + ScopeOpenID = "openid" + + // ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting + // OAuth2 refresh tokens. + // + // Support for this scope differs between OpenID Connect providers. For instance + // Google rejects it, favoring appending "access_type=offline" as part of the + // authorization request instead. + // + // See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + ScopeOfflineAccess = "offline_access" +) + +var ( + errNoAtHash = errors.New("id token did not have an access token hash") + errInvalidAtHash = errors.New("access token hash does not match value in ID token") +) + +type contextKey int + +var issuerURLKey contextKey + +// ClientContext returns a new Context that carries the provided HTTP client. +// +// This method sets the same context key used by the golang.org/x/oauth2 package, +// so the returned context works for that package too. +// +// myClient := &http.Client{} +// ctx := oidc.ClientContext(parentContext, myClient) +// +// // This will use the custom client +// provider, err := oidc.NewProvider(ctx, "https://accounts.example.com") +func ClientContext(ctx context.Context, client *http.Client) context.Context { + return context.WithValue(ctx, oauth2.HTTPClient, client) +} + +func getClient(ctx context.Context) *http.Client { + if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { + return c + } + return nil +} + +// InsecureIssuerURLContext allows discovery to work when the issuer_url reported +// by upstream is mismatched with the discovery URL. This is meant for integration +// with off-spec providers such as Azure. +// +// discoveryBaseURL := "https://login.microsoftonline.com/organizations/v2.0" +// issuerURL := "https://login.microsoftonline.com/my-tenantid/v2.0" +// +// ctx := oidc.InsecureIssuerURLContext(parentContext, issuerURL) +// +// // Provider will be discovered with the discoveryBaseURL, but use issuerURL +// // for future issuer validation. +// provider, err := oidc.NewProvider(ctx, discoveryBaseURL) +// +// This is insecure because validating the correct issuer is critical for multi-tenant +// providers. Any overrides here MUST be carefully reviewed. +func InsecureIssuerURLContext(ctx context.Context, issuerURL string) context.Context { + return context.WithValue(ctx, issuerURLKey, issuerURL) +} + +func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + client := http.DefaultClient + if c := getClient(ctx); c != nil { + client = c + } + return client.Do(req.WithContext(ctx)) +} + +// Provider represents an OpenID Connect server's configuration. +type Provider struct { + issuer string + authURL string + tokenURL string + deviceAuthURL string + userInfoURL string + jwksURL string + algorithms []string + + // Raw claims returned by the server. + rawClaims []byte + + // Guards all of the following fields. + mu sync.Mutex + // HTTP client specified from the initial NewProvider request. This is used + // when creating the common key set. + client *http.Client + // A key set that uses context.Background() and is shared between all code paths + // that don't have a convinent way of supplying a unique context. + commonRemoteKeySet KeySet +} + +func (p *Provider) remoteKeySet() KeySet { + p.mu.Lock() + defer p.mu.Unlock() + if p.commonRemoteKeySet == nil { + ctx := context.Background() + if p.client != nil { + ctx = ClientContext(ctx, p.client) + } + p.commonRemoteKeySet = NewRemoteKeySet(ctx, p.jwksURL) + } + return p.commonRemoteKeySet +} + +type providerJSON struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + DeviceAuthURL string `json:"device_authorization_endpoint"` + JWKSURL string `json:"jwks_uri"` + UserInfoURL string `json:"userinfo_endpoint"` + Algorithms []string `json:"id_token_signing_alg_values_supported"` +} + +// supportedAlgorithms is a list of algorithms explicitly supported by this +// package. If a provider supports other algorithms, such as HS256 or none, +// those values won't be passed to the IDTokenVerifier. +var supportedAlgorithms = map[string]bool{ + RS256: true, + RS384: true, + RS512: true, + ES256: true, + ES384: true, + ES512: true, + PS256: true, + PS384: true, + PS512: true, + EdDSA: true, +} + +// ProviderConfig allows creating providers when discovery isn't supported. It's +// generally easier to use NewProvider directly. +type ProviderConfig struct { + // IssuerURL is the identity of the provider, and the string it uses to sign + // ID tokens with. For example "https://accounts.google.com". This value MUST + // match ID tokens exactly. + IssuerURL string + // AuthURL is the endpoint used by the provider to support the OAuth 2.0 + // authorization endpoint. + AuthURL string + // TokenURL is the endpoint used by the provider to support the OAuth 2.0 + // token endpoint. + TokenURL string + // DeviceAuthURL is the endpoint used by the provider to support the OAuth 2.0 + // device authorization endpoint. + DeviceAuthURL string + // UserInfoURL is the endpoint used by the provider to support the OpenID + // Connect UserInfo flow. + // + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + UserInfoURL string + // JWKSURL is the endpoint used by the provider to advertise public keys to + // verify issued ID tokens. This endpoint is polled as new keys are made + // available. + JWKSURL string + + // Algorithms, if provided, indicate a list of JWT algorithms allowed to sign + // ID tokens. If not provided, this defaults to the algorithms advertised by + // the JWK endpoint, then the set of algorithms supported by this package. + Algorithms []string +} + +// NewProvider initializes a provider from a set of endpoints, rather than +// through discovery. +func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { + return &Provider{ + issuer: p.IssuerURL, + authURL: p.AuthURL, + tokenURL: p.TokenURL, + deviceAuthURL: p.DeviceAuthURL, + userInfoURL: p.UserInfoURL, + jwksURL: p.JWKSURL, + algorithms: p.Algorithms, + client: getClient(ctx), + } +} + +// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider. +// +// The issuer is the URL identifier for the service. For example: "https://accounts.google.com" +// or "https://login.salesforce.com". +func NewProvider(ctx context.Context, issuer string) (*Provider, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return nil, err + } + resp, err := doRequest(ctx, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %s", resp.Status, body) + } + + var p providerJSON + err = unmarshalResp(resp, body, &p) + if err != nil { + return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err) + } + + issuerURL, skipIssuerValidation := ctx.Value(issuerURLKey).(string) + if !skipIssuerValidation { + issuerURL = issuer + } + if p.Issuer != issuerURL && !skipIssuerValidation { + return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer) + } + var algs []string + for _, a := range p.Algorithms { + if supportedAlgorithms[a] { + algs = append(algs, a) + } + } + return &Provider{ + issuer: issuerURL, + authURL: p.AuthURL, + tokenURL: p.TokenURL, + deviceAuthURL: p.DeviceAuthURL, + userInfoURL: p.UserInfoURL, + jwksURL: p.JWKSURL, + algorithms: algs, + rawClaims: body, + client: getClient(ctx), + }, nil +} + +// Claims unmarshals raw fields returned by the server during discovery. +// +// var claims struct { +// ScopesSupported []string `json:"scopes_supported"` +// ClaimsSupported []string `json:"claims_supported"` +// } +// +// if err := provider.Claims(&claims); err != nil { +// // handle unmarshaling error +// } +// +// For a list of fields defined by the OpenID Connect spec see: +// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +func (p *Provider) Claims(v interface{}) error { + if p.rawClaims == nil { + return errors.New("oidc: claims not set") + } + return json.Unmarshal(p.rawClaims, v) +} + +// Endpoint returns the OAuth2 auth and token endpoints for the given provider. +func (p *Provider) Endpoint() oauth2.Endpoint { + return oauth2.Endpoint{AuthURL: p.authURL, DeviceAuthURL: p.deviceAuthURL, TokenURL: p.tokenURL} +} + +// UserInfoEndpoint returns the OpenID Connect userinfo endpoint for the given +// provider. +func (p *Provider) UserInfoEndpoint() string { + return p.userInfoURL +} + +// UserInfo represents the OpenID Connect userinfo claims. +type UserInfo struct { + Subject string `json:"sub"` + Profile string `json:"profile"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + + claims []byte +} + +type userInfoRaw struct { + Subject string `json:"sub"` + Profile string `json:"profile"` + Email string `json:"email"` + // Handle providers that return email_verified as a string + // https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 and + // https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 + EmailVerified stringAsBool `json:"email_verified"` +} + +// Claims unmarshals the raw JSON object claims into the provided object. +func (u *UserInfo) Claims(v interface{}) error { + if u.claims == nil { + return errors.New("oidc: claims not set") + } + return json.Unmarshal(u.claims, v) +} + +// UserInfo uses the token source to query the provider's user info endpoint. +func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) { + if p.userInfoURL == "" { + return nil, errors.New("oidc: user info endpoint is not supported by this provider") + } + + req, err := http.NewRequest("GET", p.userInfoURL, nil) + if err != nil { + return nil, fmt.Errorf("oidc: create GET request: %v", err) + } + + token, err := tokenSource.Token() + if err != nil { + return nil, fmt.Errorf("oidc: get access token: %v", err) + } + token.SetAuthHeader(req) + + resp, err := doRequest(ctx, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %s", resp.Status, body) + } + + ct := resp.Header.Get("Content-Type") + mediaType, _, parseErr := mime.ParseMediaType(ct) + if parseErr == nil && mediaType == "application/jwt" { + payload, err := p.remoteKeySet().VerifySignature(ctx, string(body)) + if err != nil { + return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err) + } + body = payload + } + + var userInfo userInfoRaw + if err := json.Unmarshal(body, &userInfo); err != nil { + return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err) + } + return &UserInfo{ + Subject: userInfo.Subject, + Profile: userInfo.Profile, + Email: userInfo.Email, + EmailVerified: bool(userInfo.EmailVerified), + claims: body, + }, nil +} + +// IDToken is an OpenID Connect extension that provides a predictable representation +// of an authorization event. +// +// The ID Token only holds fields OpenID Connect requires. To access additional +// claims returned by the server, use the Claims method. +type IDToken struct { + // The URL of the server which issued this token. OpenID Connect + // requires this value always be identical to the URL used for + // initial discovery. + // + // Note: Because of a known issue with Google Accounts' implementation + // this value may differ when using Google. + // + // See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo + Issuer string + + // The client ID, or set of client IDs, that this token is issued for. For + // common uses, this is the client that initialized the auth flow. + // + // This package ensures the audience contains an expected value. + Audience []string + + // A unique string which identifies the end user. + Subject string + + // Expiry of the token. Ths package will not process tokens that have + // expired unless that validation is explicitly turned off. + Expiry time.Time + // When the token was issued by the provider. + IssuedAt time.Time + + // Initial nonce provided during the authentication redirect. + // + // This package does NOT provided verification on the value of this field + // and it's the user's responsibility to ensure it contains a valid value. + Nonce string + + // at_hash claim, if set in the ID token. Callers can verify an access token + // that corresponds to the ID token using the VerifyAccessToken method. + AccessTokenHash string + + // signature algorithm used for ID token, needed to compute a verification hash of an + // access token + sigAlgorithm string + + // Raw payload of the id_token. + claims []byte + + // Map of distributed claim names to claim sources + distributedClaims map[string]claimSource +} + +// Claims unmarshals the raw JSON payload of the ID Token into a provided struct. +// +// idToken, err := idTokenVerifier.Verify(rawIDToken) +// if err != nil { +// // handle error +// } +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // handle error +// } +func (i *IDToken) Claims(v interface{}) error { + if i.claims == nil { + return errors.New("oidc: claims not set") + } + return json.Unmarshal(i.claims, v) +} + +// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token +// matches the hash in the id token. It returns an error if the hashes don't match. +// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token +// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken +func (i *IDToken) VerifyAccessToken(accessToken string) error { + if i.AccessTokenHash == "" { + return errNoAtHash + } + var h hash.Hash + switch i.sigAlgorithm { + case RS256, ES256, PS256: + h = sha256.New() + case RS384, ES384, PS384: + h = sha512.New384() + case RS512, ES512, PS512, EdDSA: + h = sha512.New() + default: + return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm) + } + h.Write([]byte(accessToken)) // hash documents that Write will never return an error + sum := h.Sum(nil)[:h.Size()/2] + actual := base64.RawURLEncoding.EncodeToString(sum) + if actual != i.AccessTokenHash { + return errInvalidAtHash + } + return nil +} + +type idToken struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience audience `json:"aud"` + Expiry jsonTime `json:"exp"` + IssuedAt jsonTime `json:"iat"` + NotBefore *jsonTime `json:"nbf"` + Nonce string `json:"nonce"` + AtHash string `json:"at_hash"` + ClaimNames map[string]string `json:"_claim_names"` + ClaimSources map[string]claimSource `json:"_claim_sources"` +} + +type claimSource struct { + Endpoint string `json:"endpoint"` + AccessToken string `json:"access_token"` +} + +type stringAsBool bool + +func (sb *stringAsBool) UnmarshalJSON(b []byte) error { + switch string(b) { + case "true", `"true"`: + *sb = true + case "false", `"false"`: + *sb = false + default: + return errors.New("invalid value for boolean") + } + return nil +} + +type audience []string + +func (a *audience) UnmarshalJSON(b []byte) error { + var s string + if json.Unmarshal(b, &s) == nil { + *a = audience{s} + return nil + } + var auds []string + if err := json.Unmarshal(b, &auds); err != nil { + return err + } + *a = auds + return nil +} + +type jsonTime time.Time + +func (j *jsonTime) UnmarshalJSON(b []byte) error { + var n json.Number + if err := json.Unmarshal(b, &n); err != nil { + return err + } + var unix int64 + + if t, err := n.Int64(); err == nil { + unix = t + } else { + f, err := n.Float64() + if err != nil { + return err + } + unix = int64(f) + } + *j = jsonTime(time.Unix(unix, 0)) + return nil +} + +func unmarshalResp(r *http.Response, body []byte, v interface{}) error { + err := json.Unmarshal(body, &v) + if err == nil { + return nil + } + ct := r.Header.Get("Content-Type") + mediaType, _, parseErr := mime.ParseMediaType(ct) + if parseErr == nil && mediaType == "application/json" { + return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err) + } + return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err) +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go new file mode 100644 index 0000000000..52b27b746a --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go @@ -0,0 +1,355 @@ +package oidc + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + jose "github.com/go-jose/go-jose/v4" + "golang.org/x/oauth2" +) + +const ( + issuerGoogleAccounts = "https://accounts.google.com" + issuerGoogleAccountsNoScheme = "accounts.google.com" +) + +// TokenExpiredError indicates that Verify failed because the token was expired. This +// error does NOT indicate that the token is not also invalid for other reasons. Other +// checks might have failed if the expiration check had not failed. +type TokenExpiredError struct { + // Expiry is the time when the token expired. + Expiry time.Time +} + +func (e *TokenExpiredError) Error() string { + return fmt.Sprintf("oidc: token is expired (Token Expiry: %v)", e.Expiry) +} + +// KeySet is a set of publc JSON Web Keys that can be used to validate the signature +// of JSON web tokens. This is expected to be backed by a remote key set through +// provider metadata discovery or an in-memory set of keys delivered out-of-band. +type KeySet interface { + // VerifySignature parses the JSON web token, verifies the signature, and returns + // the raw payload. Header and claim fields are validated by other parts of the + // package. For example, the KeySet does not need to check values such as signature + // algorithm, issuer, and audience since the IDTokenVerifier validates these values + // independently. + // + // If VerifySignature makes HTTP requests to verify the token, it's expected to + // use any HTTP client associated with the context through ClientContext. + VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) +} + +// IDTokenVerifier provides verification for ID Tokens. +type IDTokenVerifier struct { + keySet KeySet + config *Config + issuer string +} + +// NewVerifier returns a verifier manually constructed from a key set and issuer URL. +// +// It's easier to use provider discovery to construct an IDTokenVerifier than creating +// one directly. This method is intended to be used with provider that don't support +// metadata discovery, or avoiding round trips when the key set URL is already known. +// +// This constructor can be used to create a verifier directly using the issuer URL and +// JSON Web Key Set URL without using discovery: +// +// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs") +// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config) +// +// Or a static key set (e.g. for testing): +// +// keySet := &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{pub1, pub2}} +// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config) +func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier { + return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL} +} + +// Config is the configuration for an IDTokenVerifier. +type Config struct { + // Expected audience of the token. For a majority of the cases this is expected to be + // the ID of the client that initialized the login flow. It may occasionally differ if + // the provider supports the authorizing party (azp) claim. + // + // If not provided, users must explicitly set SkipClientIDCheck. + ClientID string + // If specified, only this set of algorithms may be used to sign the JWT. + // + // If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this + // defaults to the set of algorithms the provider supports. Otherwise this values + // defaults to RS256. + SupportedSigningAlgs []string + + // If true, no ClientID check performed. Must be true if ClientID field is empty. + SkipClientIDCheck bool + // If true, token expiry is not checked. + SkipExpiryCheck bool + + // SkipIssuerCheck is intended for specialized cases where the the caller wishes to + // defer issuer validation. When enabled, callers MUST independently verify the Token's + // Issuer is a known good value. + // + // Mismatched issuers often indicate client mis-configuration. If mismatches are + // unexpected, evaluate if the provided issuer URL is incorrect instead of enabling + // this option. + SkipIssuerCheck bool + + // Time function to check Token expiry. Defaults to time.Now + Now func() time.Time + + // InsecureSkipSignatureCheck causes this package to skip JWT signature validation. + // It's intended for special cases where providers (such as Azure), use the "none" + // algorithm. + // + // This option can only be enabled safely when the ID Token is received directly + // from the provider after the token exchange. + // + // This option MUST NOT be used when receiving an ID Token from sources other + // than the token endpoint. + InsecureSkipSignatureCheck bool +} + +// VerifierContext returns an IDTokenVerifier that uses the provider's key set to +// verify JWTs. As opposed to Verifier, the context is used to configure requests +// to the upstream JWKs endpoint. The provided context's cancellation is ignored. +func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDTokenVerifier { + return p.newVerifier(NewRemoteKeySet(ctx, p.jwksURL), config) +} + +// Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs. +// +// The returned verifier uses a background context for all requests to the upstream +// JWKs endpoint. To control that context, use VerifierContext instead. +func (p *Provider) Verifier(config *Config) *IDTokenVerifier { + return p.newVerifier(p.remoteKeySet(), config) +} + +func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier { + if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 { + // Make a copy so we don't modify the config values. + cp := &Config{} + *cp = *config + cp.SupportedSigningAlgs = p.algorithms + config = cp + } + return NewVerifier(p.issuer, keySet, config) +} + +func parseJWT(p string) ([]byte, error) { + parts := strings.Split(p, ".") + if len(parts) < 2 { + return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err) + } + return payload, nil +} + +func contains(sli []string, ele string) bool { + for _, s := range sli { + if s == ele { + return true + } + } + return false +} + +// Returns the Claims from the distributed JWT token +func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) { + req, err := http.NewRequest("GET", src.Endpoint, nil) + if err != nil { + return nil, fmt.Errorf("malformed request: %v", err) + } + if src.AccessToken != "" { + req.Header.Set("Authorization", "Bearer "+src.AccessToken) + } + + resp, err := doRequest(ctx, req) + if err != nil { + return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode) + } + + token, err := verifier.Verify(ctx, string(body)) + if err != nil { + return nil, fmt.Errorf("malformed response body: %v", err) + } + + return token.claims, nil +} + +// Verify parses a raw ID Token, verifies it's been signed by the provider, performs +// any additional checks depending on the Config, and returns the payload. +// +// Verify does NOT do nonce validation, which is the callers responsibility. +// +// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +// +// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code")) +// if err != nil { +// // handle error +// } +// +// // Extract the ID Token from oauth2 token. +// rawIDToken, ok := oauth2Token.Extra("id_token").(string) +// if !ok { +// // handle error +// } +// +// token, err := verifier.Verify(ctx, rawIDToken) +func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { + // Throw out tokens with invalid claims before trying to verify the token. This lets + // us do cheap checks before possibly re-syncing keys. + payload, err := parseJWT(rawIDToken) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %v", err) + } + var token idToken + if err := json.Unmarshal(payload, &token); err != nil { + return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) + } + + distributedClaims := make(map[string]claimSource) + + //step through the token to map claim names to claim sources" + for cn, src := range token.ClaimNames { + if src == "" { + return nil, fmt.Errorf("oidc: failed to obtain source from claim name") + } + s, ok := token.ClaimSources[src] + if !ok { + return nil, fmt.Errorf("oidc: source does not exist") + } + distributedClaims[cn] = s + } + + t := &IDToken{ + Issuer: token.Issuer, + Subject: token.Subject, + Audience: []string(token.Audience), + Expiry: time.Time(token.Expiry), + IssuedAt: time.Time(token.IssuedAt), + Nonce: token.Nonce, + AccessTokenHash: token.AtHash, + claims: payload, + distributedClaims: distributedClaims, + } + + // Check issuer. + if !v.config.SkipIssuerCheck && t.Issuer != v.issuer { + // Google sometimes returns "accounts.google.com" as the issuer claim instead of + // the required "https://accounts.google.com". Detect this case and allow it only + // for Google. + // + // We will not add hooks to let other providers go off spec like this. + if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) { + return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer) + } + } + + // If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty. + // + // This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party). + if !v.config.SkipClientIDCheck { + if v.config.ClientID != "" { + if !contains(t.Audience, v.config.ClientID) { + return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) + } + } else { + return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set") + } + } + + // If a SkipExpiryCheck is false, make sure token is not expired. + if !v.config.SkipExpiryCheck { + now := time.Now + if v.config.Now != nil { + now = v.config.Now + } + nowTime := now() + + if t.Expiry.Before(nowTime) { + return nil, &TokenExpiredError{Expiry: t.Expiry} + } + + // If nbf claim is provided in token, ensure that it is indeed in the past. + if token.NotBefore != nil { + nbfTime := time.Time(*token.NotBefore) + // Set to 5 minutes since this is what other OpenID Connect providers do to deal with clock skew. + // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/6.12.2/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs#L149-L153 + leeway := 5 * time.Minute + + if nowTime.Add(leeway).Before(nbfTime) { + return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime) + } + } + } + + if v.config.InsecureSkipSignatureCheck { + return t, nil + } + + var supportedSigAlgs []jose.SignatureAlgorithm + for _, alg := range v.config.SupportedSigningAlgs { + supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) + } + if len(supportedSigAlgs) == 0 { + // If no algorithms were specified by both the config and discovery, default + // to the one mandatory algorithm "RS256". + supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} + } + jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %v", err) + } + + switch len(jws.Signatures) { + case 0: + return nil, fmt.Errorf("oidc: id token not signed") + case 1: + default: + return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") + } + sig := jws.Signatures[0] + t.sigAlgorithm = sig.Header.Algorithm + + ctx = context.WithValue(ctx, parsedJWTKey, jws) + gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("failed to verify signature: %v", err) + } + + // Ensure that the payload returned by the square actually matches the payload parsed earlier. + if !bytes.Equal(gotPayload, payload) { + return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") + } + + return t, nil +} + +// Nonce returns an auth code option which requires the ID Token created by the +// OpenID Connect provider to contain the specified nonce. +func Nonce(nonce string) oauth2.AuthCodeOption { + return oauth2.SetAuthURLParam("nonce", nonce) +} diff --git a/vendor/github.com/go-jose/go-jose/v4/.gitignore b/vendor/github.com/go-jose/go-jose/v4/.gitignore new file mode 100644 index 0000000000..eb29ebaefd --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/.gitignore @@ -0,0 +1,2 @@ +jose-util/jose-util +jose-util.t.err \ No newline at end of file diff --git a/vendor/github.com/go-jose/go-jose/v4/.golangci.yml b/vendor/github.com/go-jose/go-jose/v4/.golangci.yml new file mode 100644 index 0000000000..2a577a8f95 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/.golangci.yml @@ -0,0 +1,53 @@ +# https://github.com/golangci/golangci-lint + +run: + skip-files: + - doc_test.go + modules-download-mode: readonly + +linters: + enable-all: true + disable: + - gochecknoglobals + - goconst + - lll + - maligned + - nakedret + - scopelint + - unparam + - funlen # added in 1.18 (requires go-jose changes before it can be enabled) + +linters-settings: + gocyclo: + min-complexity: 35 + +issues: + exclude-rules: + - text: "don't use ALL_CAPS in Go names" + linters: + - golint + - text: "hardcoded credentials" + linters: + - gosec + - text: "weak cryptographic primitive" + linters: + - gosec + - path: json/ + linters: + - dupl + - errcheck + - gocritic + - gocyclo + - golint + - govet + - ineffassign + - staticcheck + - structcheck + - stylecheck + - unused + - path: _test\.go + linters: + - scopelint + - path: jwk.go + linters: + - gocyclo diff --git a/vendor/github.com/go-jose/go-jose/v4/.travis.yml b/vendor/github.com/go-jose/go-jose/v4/.travis.yml new file mode 100644 index 0000000000..48de631b00 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/.travis.yml @@ -0,0 +1,33 @@ +language: go + +matrix: + fast_finish: true + allow_failures: + - go: tip + +go: + - "1.13.x" + - "1.14.x" + - tip + +before_script: + - export PATH=$HOME/.local/bin:$PATH + +before_install: + - go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0 + - pip install cram --user + +script: + - go test -v -covermode=count -coverprofile=profile.cov . + - go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner + - go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher + - go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt + - go test -v ./json # no coverage for forked encoding/json package + - golangci-lint run + - cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util + - cd .. + +after_success: + - gocovmerge *.cov */*.cov > merged.coverprofile + - goveralls -coverprofile merged.coverprofile -service=travis-ci diff --git a/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md b/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md new file mode 100644 index 0000000000..246979f161 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md @@ -0,0 +1,89 @@ +# v4.0.3 + +## Changed + + - Allow unmarshalling JSONWebKeySets with unsupported key types (#130) + - Document that OpaqueKeyEncrypter can't be implemented (for now) (#129) + - Dependency updates + +# v4.0.2 + +## Changed + + - Improved documentation of Verify() to note that JSONWebKeySet is a supported + argument type (#104) + - Defined exported error values for missing x5c header and unsupported elliptic + curves error cases (#117) + +# v4.0.1 + +## Fixed + + - An attacker could send a JWE containing compressed data that used large + amounts of memory and CPU when decompressed by `Decrypt` or `DecryptMulti`. + Those functions now return an error if the decompressed data would exceed + 250kB or 10x the compressed size (whichever is larger). Thanks to + Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj) + for reporting. + +# v4.0.0 + +This release makes some breaking changes in order to more thoroughly +address the vulnerabilities discussed in [Three New Attacks Against JSON Web +Tokens][1], "Sign/encrypt confusion", "Billion hash attack", and "Polyglot +token". + +## Changed + + - Limit JWT encryption types (exclude password or public key types) (#78) + - Enforce minimum length for HMAC keys (#85) + - jwt: match any audience in a list, rather than requiring all audiences (#81) + - jwt: accept only Compact Serialization (#75) + - jws: Add expected algorithms for signatures (#74) + - Require specifying expected algorithms for ParseEncrypted, + ParseSigned, ParseDetached, jwt.ParseEncrypted, jwt.ParseSigned, + jwt.ParseSignedAndEncrypted (#69, #74) + - Usually there is a small, known set of appropriate algorithms for a program + to use and it's a mistake to allow unexpected algorithms. For instance the + "billion hash attack" relies in part on programs accepting the PBES2 + encryption algorithm and doing the necessary work even if they weren't + specifically configured to allow PBES2. + - Revert "Strip padding off base64 strings" (#82) + - The specs require base64url encoding without padding. + - Minimum supported Go version is now 1.21 + +## Added + + - ParseSignedCompact, ParseSignedJSON, ParseEncryptedCompact, ParseEncryptedJSON. + - These allow parsing a specific serialization, as opposed to ParseSigned and + ParseEncrypted, which try to automatically detect which serialization was + provided. It's common to require a specific serialization for a specific + protocol - for instance JWT requires Compact serialization. + +[1]: https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf + +# v3.0.2 + +## Fixed + + - DecryptMulti: handle decompression error (#19) + +## Changed + + - jwe/CompactSerialize: improve performance (#67) + - Increase the default number of PBKDF2 iterations to 600k (#48) + - Return the proper algorithm for ECDSA keys (#45) + +## Added + + - Add Thumbprint support for opaque signers (#38) + +# v3.0.1 + +## Fixed + + - Security issue: an attacker specifying a large "p2c" value can cause + JSONWebEncryption.Decrypt and JSONWebEncryption.DecryptMulti to consume large + amounts of CPU, causing a DoS. Thanks to Matt Schwager (@mschwager) for the + disclosure and to Tom Tervoort for originally publishing the category of attack. + https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf diff --git a/vendor/github.com/go-jose/go-jose/v4/CONTRIBUTING.md b/vendor/github.com/go-jose/go-jose/v4/CONTRIBUTING.md new file mode 100644 index 0000000000..b63e1f8fee --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing + +If you would like to contribute code to go-jose you can do so through GitHub by +forking the repository and sending a pull request. + +When submitting code, please make every effort to follow existing conventions +and style in order to keep the code as readable as possible. Please also make +sure all tests pass by running `go test`, and format your code with `go fmt`. +We also recommend using `golint` and `errcheck`. + +Before your code can be accepted into the project you must also sign the +Individual Contributor License Agreement. We use [cla-assistant.io][1] and you +will be prompted to sign once a pull request is opened. + +[1]: https://cla-assistant.io/ diff --git a/vendor/google.golang.org/appengine/LICENSE b/vendor/github.com/go-jose/go-jose/v4/LICENSE similarity index 100% rename from vendor/google.golang.org/appengine/LICENSE rename to vendor/github.com/go-jose/go-jose/v4/LICENSE diff --git a/vendor/github.com/go-jose/go-jose/v4/README.md b/vendor/github.com/go-jose/go-jose/v4/README.md new file mode 100644 index 0000000000..79a7c5ecc8 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/README.md @@ -0,0 +1,114 @@ +# Go JOSE + +[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) +[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) +[![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE) +[![test](https://img.shields.io/github/checks-status/go-jose/go-jose/v4)](https://github.com/go-jose/go-jose/actions) + +Package jose aims to provide an implementation of the Javascript Object Signing +and Encryption set of standards. This includes support for JSON Web Encryption, +JSON Web Signature, and JSON Web Token standards. + +**Disclaimer**: This library contains encryption software that is subject to +the U.S. Export Administration Regulations. You may not export, re-export, +transfer or download this code or any part of it in violation of any United +States law, directive or regulation. In particular this software may not be +exported or re-exported in any form or on any media to Iran, North Sudan, +Syria, Cuba, or North Korea, or to denied persons or entities mentioned on any +US maintained blocked list. + +## Overview + +The implementation follows the +[JSON Web Encryption](https://dx.doi.org/10.17487/RFC7516) (RFC 7516), +[JSON Web Signature](https://dx.doi.org/10.17487/RFC7515) (RFC 7515), and +[JSON Web Token](https://dx.doi.org/10.17487/RFC7519) (RFC 7519) specifications. +Tables of supported algorithms are shown below. The library supports both +the compact and JWS/JWE JSON Serialization formats, and has optional support for +multiple recipients. It also comes with a small command-line utility +([`jose-util`](https://pkg.go.dev/github.com/go-jose/go-jose/jose-util)) +for dealing with JOSE messages in a shell. + +**Note**: We use a forked version of the `encoding/json` package from the Go +standard library which uses case-sensitive matching for member names (instead +of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)). +This is to avoid differences in interpretation of messages between go-jose and +libraries in other languages. + +### Versions + +[Version 4](https://github.com/go-jose/go-jose) +([branch](https://github.com/go-jose/go-jose/tree/main), +[doc](https://pkg.go.dev/github.com/go-jose/go-jose/v4), [releases](https://github.com/go-jose/go-jose/releases)) is the current stable version: + + import "github.com/go-jose/go-jose/v4" + +The old [square/go-jose](https://github.com/square/go-jose) repo contains the prior v1 and v2 versions, which +are still useable but not actively developed anymore. + +Version 3, in this repo, is still receiving security fixes but not functionality +updates. + +### Supported algorithms + +See below for a table of supported algorithms. Algorithm identifiers match +the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518) +standard where possible. The Godoc reference has a list of constants. + + Key encryption | Algorithm identifier(s) + :------------------------- | :------------------------------ + RSA-PKCS#1v1.5 | RSA1_5 + RSA-OAEP | RSA-OAEP, RSA-OAEP-256 + AES key wrap | A128KW, A192KW, A256KW + AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW + ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW + ECDH-ES (direct) | ECDH-ES1 + Direct encryption | dir1 + +1. Not supported in multi-recipient mode + + Signing / MAC | Algorithm identifier(s) + :------------------------- | :------------------------------ + RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 + RSASSA-PSS | PS256, PS384, PS512 + HMAC | HS256, HS384, HS512 + ECDSA | ES256, ES384, ES512 + Ed25519 | EdDSA2 + +2. Only available in version 2 of the package + + Content encryption | Algorithm identifier(s) + :------------------------- | :------------------------------ + AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 + AES-GCM | A128GCM, A192GCM, A256GCM + + Compression | Algorithm identifiers(s) + :------------------------- | ------------------------------- + DEFLATE (RFC 1951) | DEF + +### Supported key types + +See below for a table of supported key types. These are understood by the +library, and can be passed to corresponding functions such as `NewEncrypter` or +`NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which +allows attaching a key id. + + Algorithm(s) | Corresponding types + :------------------------- | ------------------------------- + RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) + ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) + EdDSA1 | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) + AES, HMAC | []byte + +1. Only available in version 2 or later of the package + +## Examples + +[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) +[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) + +Examples can be found in the Godoc +reference for this package. The +[`jose-util`](https://github.com/go-jose/go-jose/tree/v4/jose-util) +subdirectory also contains a small command-line utility which might be useful +as an example as well. diff --git a/vendor/github.com/go-jose/go-jose/v4/SECURITY.md b/vendor/github.com/go-jose/go-jose/v4/SECURITY.md new file mode 100644 index 0000000000..2f18a75a82 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy +This document explains how to contact the Let's Encrypt security team to report security vulnerabilities. + +## Supported Versions +| Version | Supported | +| ------- | ----------| +| >= v3 | ✓ | +| v2 | ✗ | +| v1 | ✗ | + +## Reporting a vulnerability + +Please see [https://letsencrypt.org/contact/#security](https://letsencrypt.org/contact/#security) for the email address to report a vulnerability. Ensure that the subject line for your report contains the word `vulnerability` and is descriptive. Your email should be acknowledged within 24 hours. If you do not receive a response within 24 hours, please follow-up again with another email. diff --git a/vendor/github.com/go-jose/go-jose/v4/asymmetric.go b/vendor/github.com/go-jose/go-jose/v4/asymmetric.go new file mode 100644 index 0000000000..f8d5774ef5 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/asymmetric.go @@ -0,0 +1,595 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 jose + +import ( + "crypto" + "crypto/aes" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "errors" + "fmt" + "math/big" + + josecipher "github.com/go-jose/go-jose/v4/cipher" + "github.com/go-jose/go-jose/v4/json" +) + +// A generic RSA-based encrypter/verifier +type rsaEncrypterVerifier struct { + publicKey *rsa.PublicKey +} + +// A generic RSA-based decrypter/signer +type rsaDecrypterSigner struct { + privateKey *rsa.PrivateKey +} + +// A generic EC-based encrypter/verifier +type ecEncrypterVerifier struct { + publicKey *ecdsa.PublicKey +} + +type edEncrypterVerifier struct { + publicKey ed25519.PublicKey +} + +// A key generator for ECDH-ES +type ecKeyGenerator struct { + size int + algID string + publicKey *ecdsa.PublicKey +} + +// A generic EC-based decrypter/signer +type ecDecrypterSigner struct { + privateKey *ecdsa.PrivateKey +} + +type edDecrypterSigner struct { + privateKey ed25519.PrivateKey +} + +// newRSARecipient creates recipientKeyInfo based on the given key. +func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case RSA1_5, RSA_OAEP, RSA_OAEP_256: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &rsaEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newRSASigner creates a recipientSigInfo based on the given key. +func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case RS256, RS384, RS512, PS256, PS384, PS512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &rsaDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { + if sigAlg != EdDSA { + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &edDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// newECDHRecipient creates recipientKeyInfo based on the given key. +func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &ecEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newECDSASigner creates a recipientSigInfo based on the given key. +func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case ES256, ES384, ES512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: staticPublicKey(&JSONWebKey{ + Key: privateKey.Public(), + }), + signer: &ecDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// Encrypt the given payload and update the object. +func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + encryptedKey, err := ctx.encrypt(cek, alg) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: encryptedKey, + header: &rawHeader{}, + }, nil +} + +// Encrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { + switch alg { + case RSA1_5: + return rsa.EncryptPKCS1v15(RandReader, ctx.publicKey, cek) + case RSA_OAEP: + return rsa.EncryptOAEP(sha1.New(), RandReader, ctx.publicKey, cek, []byte{}) + case RSA_OAEP_256: + return rsa.EncryptOAEP(sha256.New(), RandReader, ctx.publicKey, cek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Decrypt the given payload and return the content encryption key. +func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) +} + +// Decrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { + // Note: The random reader on decrypt operations is only used for blinding, + // so stubbing is meanlingless (hence the direct use of rand.Reader). + switch alg { + case RSA1_5: + defer func() { + // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload + // because of an index out of bounds error, which we want to ignore. + // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() + // only exists for preventing crashes with unpatched versions. + // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k + // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 + _ = recover() + }() + + // Perform some input validation. + keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 + if keyBytes != len(jek) { + // Input size is incorrect, the encrypted payload should always match + // the size of the public modulus (e.g. using a 2048 bit key will + // produce 256 bytes of output). Reject this since it's invalid input. + return nil, ErrCryptoFailure + } + + cek, _, err := generator.genKey() + if err != nil { + return nil, ErrCryptoFailure + } + + // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to + // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing + // the Million Message Attack on Cryptographic Message Syntax". We are + // therefore deliberately ignoring errors here. + _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) + + return cek, nil + case RSA_OAEP: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + case RSA_OAEP_256: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Sign the given payload +func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return Signature{}, ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + var out []byte + var err error + + switch alg { + case RS256, RS384, RS512: + // TODO(https://github.com/go-jose/go-jose/issues/40): As of go1.20, the + // random parameter is legacy and ignored, and it can be nil. + // https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/rsa/pkcs1v15.go;l=263;bpv=0;bpt=1 + out, err = rsa.SignPKCS1v15(RandReader, ctx.privateKey, hash, hashed) + case PS256, PS384, PS512: + out, err = rsa.SignPSS(RandReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + }) + } + + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + switch alg { + case RS256, RS384, RS512: + return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) + case PS256, PS384, PS512: + return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) + } + + return ErrUnsupportedAlgorithm +} + +// Encrypt the given payload and update the object. +func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + switch alg { + case ECDH_ES: + // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. + return recipientInfo{ + header: &rawHeader{}, + }, nil + case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientInfo{}, ErrUnsupportedAlgorithm + } + + generator := ecKeyGenerator{ + algID: string(alg), + publicKey: ctx.publicKey, + } + + switch alg { + case ECDH_ES_A128KW: + generator.size = 16 + case ECDH_ES_A192KW: + generator.size = 24 + case ECDH_ES_A256KW: + generator.size = 32 + } + + kek, header, err := generator.genKey() + if err != nil { + return recipientInfo{}, err + } + + block, err := aes.NewCipher(kek) + if err != nil { + return recipientInfo{}, err + } + + jek, err := josecipher.KeyWrap(block, cek) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: jek, + header: &header, + }, nil +} + +// Get key size for EC key generator +func (ctx ecKeyGenerator) keySize() int { + return ctx.size +} + +// Get a content encryption key for ECDH-ES +func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { + priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, RandReader) + if err != nil { + return nil, rawHeader{}, err + } + + out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) + + b, err := json.Marshal(&JSONWebKey{ + Key: &priv.PublicKey, + }) + if err != nil { + return nil, nil, err + } + + headers := rawHeader{ + headerEPK: makeRawMessage(b), + } + + return out, headers, nil +} + +// Decrypt the given payload and return the content encryption key. +func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + epk, err := headers.getEPK() + if err != nil { + return nil, errors.New("go-jose/go-jose: invalid epk header") + } + if epk == nil { + return nil, errors.New("go-jose/go-jose: missing epk header") + } + + publicKey, ok := epk.Key.(*ecdsa.PublicKey) + if publicKey == nil || !ok { + return nil, errors.New("go-jose/go-jose: invalid epk header") + } + + if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return nil, errors.New("go-jose/go-jose: invalid public key in epk header") + } + + apuData, err := headers.getAPU() + if err != nil { + return nil, errors.New("go-jose/go-jose: invalid apu header") + } + apvData, err := headers.getAPV() + if err != nil { + return nil, errors.New("go-jose/go-jose: invalid apv header") + } + + deriveKey := func(algID string, size int) []byte { + return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) + } + + var keySize int + + algorithm := headers.getAlgorithm() + switch algorithm { + case ECDH_ES: + // ECDH-ES uses direct key agreement, no key unwrapping necessary. + return deriveKey(string(headers.getEncryption()), generator.keySize()), nil + case ECDH_ES_A128KW: + keySize = 16 + case ECDH_ES_A192KW: + keySize = 24 + case ECDH_ES_A256KW: + keySize = 32 + default: + return nil, ErrUnsupportedAlgorithm + } + + key := deriveKey(string(algorithm), keySize) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + return josecipher.KeyUnwrap(block, recipient.encryptedKey) +} + +func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + if alg != EdDSA { + return Signature{}, ErrUnsupportedAlgorithm + } + + sig, err := ctx.privateKey.Sign(RandReader, payload, crypto.Hash(0)) + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: sig, + protected: &rawHeader{}, + }, nil +} + +func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + if alg != EdDSA { + return ErrUnsupportedAlgorithm + } + ok := ed25519.Verify(ctx.publicKey, payload, signature) + if !ok { + return errors.New("go-jose/go-jose: ed25519 signature failed to verify") + } + return nil +} + +// Sign the given payload +func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var expectedBitSize int + var hash crypto.Hash + + switch alg { + case ES256: + expectedBitSize = 256 + hash = crypto.SHA256 + case ES384: + expectedBitSize = 384 + hash = crypto.SHA384 + case ES512: + expectedBitSize = 521 + hash = crypto.SHA512 + } + + curveBits := ctx.privateKey.Curve.Params().BitSize + if expectedBitSize != curveBits { + return Signature{}, fmt.Errorf("go-jose/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r, s, err := ecdsa.Sign(RandReader, ctx.privateKey, hashed) + if err != nil { + return Signature{}, err + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + + // We serialize the outputs (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + out := append(rBytesPadded, sBytesPadded...) + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var keySize int + var hash crypto.Hash + + switch alg { + case ES256: + keySize = 32 + hash = crypto.SHA256 + case ES384: + keySize = 48 + hash = crypto.SHA384 + case ES512: + keySize = 66 + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + if len(signature) != 2*keySize { + return fmt.Errorf("go-jose/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r := big.NewInt(0).SetBytes(signature[:keySize]) + s := big.NewInt(0).SetBytes(signature[keySize:]) + + match := ecdsa.Verify(ctx.publicKey, hashed, r, s) + if !match { + return errors.New("go-jose/go-jose: ecdsa signature failed to verify") + } + + return nil +} diff --git a/vendor/github.com/go-jose/go-jose/v4/cipher/cbc_hmac.go b/vendor/github.com/go-jose/go-jose/v4/cipher/cbc_hmac.go new file mode 100644 index 0000000000..af029cec0b --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/cipher/cbc_hmac.go @@ -0,0 +1,196 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 josecipher + +import ( + "bytes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/binary" + "errors" + "hash" +) + +const ( + nonceBytes = 16 +) + +// NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. +func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { + keySize := len(key) / 2 + integrityKey := key[:keySize] + encryptionKey := key[keySize:] + + blockCipher, err := newBlockCipher(encryptionKey) + if err != nil { + return nil, err + } + + var hash func() hash.Hash + switch keySize { + case 16: + hash = sha256.New + case 24: + hash = sha512.New384 + case 32: + hash = sha512.New + } + + return &cbcAEAD{ + hash: hash, + blockCipher: blockCipher, + authtagBytes: keySize, + integrityKey: integrityKey, + }, nil +} + +// An AEAD based on CBC+HMAC +type cbcAEAD struct { + hash func() hash.Hash + authtagBytes int + integrityKey []byte + blockCipher cipher.Block +} + +func (ctx *cbcAEAD) NonceSize() int { + return nonceBytes +} + +func (ctx *cbcAEAD) Overhead() int { + // Maximum overhead is block size (for padding) plus auth tag length, where + // the length of the auth tag is equivalent to the key size. + return ctx.blockCipher.BlockSize() + ctx.authtagBytes +} + +// Seal encrypts and authenticates the plaintext. +func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { + // Output buffer -- must take care not to mangle plaintext input. + ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] + copy(ciphertext, plaintext) + ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) + + cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) + + cbc.CryptBlocks(ciphertext, ciphertext) + authtag := ctx.computeAuthTag(data, nonce, ciphertext) + + ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) + copy(out, ciphertext) + copy(out[len(ciphertext):], authtag) + + return ret +} + +// Open decrypts and authenticates the ciphertext. +func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + if len(ciphertext) < ctx.authtagBytes { + return nil, errors.New("go-jose/go-jose: invalid ciphertext (too short)") + } + + offset := len(ciphertext) - ctx.authtagBytes + expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) + match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) + if match != 1 { + return nil, errors.New("go-jose/go-jose: invalid ciphertext (auth tag mismatch)") + } + + cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) + + // Make copy of ciphertext buffer, don't want to modify in place + buffer := append([]byte{}, ciphertext[:offset]...) + + if len(buffer)%ctx.blockCipher.BlockSize() > 0 { + return nil, errors.New("go-jose/go-jose: invalid ciphertext (invalid length)") + } + + cbc.CryptBlocks(buffer, buffer) + + // Remove padding + plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) + if err != nil { + return nil, err + } + + ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) + copy(out, plaintext) + + return ret, nil +} + +// Compute an authentication tag +func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { + buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) + n := 0 + n += copy(buffer, aad) + n += copy(buffer[n:], nonce) + n += copy(buffer[n:], ciphertext) + binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) + + // According to documentation, Write() on hash.Hash never fails. + hmac := hmac.New(ctx.hash, ctx.integrityKey) + _, _ = hmac.Write(buffer) + + return hmac.Sum(nil)[:ctx.authtagBytes] +} + +// resize ensures that the given slice has a capacity of at least n bytes. +// If the capacity of the slice is less than n, a new slice is allocated +// and the existing data will be copied. +func resize(in []byte, n uint64) (head, tail []byte) { + if uint64(cap(in)) >= n { + head = in[:n] + } else { + head = make([]byte, n) + copy(head, in) + } + + tail = head[len(in):] + return +} + +// Apply padding +func padBuffer(buffer []byte, blockSize int) []byte { + missing := blockSize - (len(buffer) % blockSize) + ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) + padding := bytes.Repeat([]byte{byte(missing)}, missing) + copy(out, padding) + return ret +} + +// Remove padding +func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { + if len(buffer)%blockSize != 0 { + return nil, errors.New("go-jose/go-jose: invalid padding") + } + + last := buffer[len(buffer)-1] + count := int(last) + + if count == 0 || count > blockSize || count > len(buffer) { + return nil, errors.New("go-jose/go-jose: invalid padding") + } + + padding := bytes.Repeat([]byte{last}, count) + if !bytes.HasSuffix(buffer, padding) { + return nil, errors.New("go-jose/go-jose: invalid padding") + } + + return buffer[:len(buffer)-count], nil +} diff --git a/vendor/github.com/go-jose/go-jose/v4/cipher/concat_kdf.go b/vendor/github.com/go-jose/go-jose/v4/cipher/concat_kdf.go new file mode 100644 index 0000000000..f62c3bdba5 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/cipher/concat_kdf.go @@ -0,0 +1,75 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 josecipher + +import ( + "crypto" + "encoding/binary" + "hash" + "io" +) + +type concatKDF struct { + z, info []byte + i uint32 + cache []byte + hasher hash.Hash +} + +// NewConcatKDF builds a KDF reader based on the given inputs. +func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { + buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) + n := 0 + n += copy(buffer, algID) + n += copy(buffer[n:], ptyUInfo) + n += copy(buffer[n:], ptyVInfo) + n += copy(buffer[n:], supPubInfo) + copy(buffer[n:], supPrivInfo) + + hasher := hash.New() + + return &concatKDF{ + z: z, + info: buffer, + hasher: hasher, + cache: []byte{}, + i: 1, + } +} + +func (ctx *concatKDF) Read(out []byte) (int, error) { + copied := copy(out, ctx.cache) + ctx.cache = ctx.cache[copied:] + + for copied < len(out) { + ctx.hasher.Reset() + + // Write on a hash.Hash never fails + _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) + _, _ = ctx.hasher.Write(ctx.z) + _, _ = ctx.hasher.Write(ctx.info) + + hash := ctx.hasher.Sum(nil) + chunkCopied := copy(out[copied:], hash) + copied += chunkCopied + ctx.cache = hash[chunkCopied:] + + ctx.i++ + } + + return copied, nil +} diff --git a/vendor/github.com/go-jose/go-jose/v4/cipher/ecdh_es.go b/vendor/github.com/go-jose/go-jose/v4/cipher/ecdh_es.go new file mode 100644 index 0000000000..093c646740 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/cipher/ecdh_es.go @@ -0,0 +1,86 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 josecipher + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/binary" +) + +// DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. +// It is an error to call this function with a private/public key that are not on the same +// curve. Callers must ensure that the keys are valid before calling this function. Output +// size may be at most 1<<16 bytes (64 KiB). +func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { + if size > 1<<16 { + panic("ECDH-ES output size too large, must be less than or equal to 1<<16") + } + + // algId, partyUInfo, partyVInfo inputs must be prefixed with the length + algID := lengthPrefixed([]byte(alg)) + ptyUInfo := lengthPrefixed(apuData) + ptyVInfo := lengthPrefixed(apvData) + + // suppPubInfo is the encoded length of the output size in bits + supPubInfo := make([]byte, 4) + binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) + + if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { + panic("public key not on same curve as private key") + } + + z, _ := priv.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) + zBytes := z.Bytes() + + // Note that calling z.Bytes() on a big.Int may strip leading zero bytes from + // the returned byte array. This can lead to a problem where zBytes will be + // shorter than expected which breaks the key derivation. Therefore we must pad + // to the full length of the expected coordinate here before calling the KDF. + octSize := dSize(priv.Curve) + if len(zBytes) != octSize { + zBytes = append(bytes.Repeat([]byte{0}, octSize-len(zBytes)), zBytes...) + } + + reader := NewConcatKDF(crypto.SHA256, zBytes, algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) + key := make([]byte, size) + + // Read on the KDF will never fail + _, _ = reader.Read(key) + + return key +} + +// dSize returns the size in octets for a coordinate on a elliptic curve. +func dSize(curve elliptic.Curve) int { + order := curve.Params().P + bitLen := order.BitLen() + size := bitLen / 8 + if bitLen%8 != 0 { + size++ + } + return size +} + +func lengthPrefixed(data []byte) []byte { + out := make([]byte, len(data)+4) + binary.BigEndian.PutUint32(out, uint32(len(data))) + copy(out[4:], data) + return out +} diff --git a/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go b/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go new file mode 100644 index 0000000000..b9effbca8a --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go @@ -0,0 +1,109 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 josecipher + +import ( + "crypto/cipher" + "crypto/subtle" + "encoding/binary" + "errors" +) + +var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} + +// KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. +func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { + if len(cek)%8 != 0 { + return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") + } + + n := len(cek) / 8 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], cek[i*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer, defaultIV) + + for t := 0; t < 6*n; t++ { + copy(buffer[8:], r[t%n]) + + block.Encrypt(buffer, buffer) + + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] ^= tBytes[i] + } + copy(r[t%n], buffer[8:]) + } + + out := make([]byte, (n+1)*8) + copy(out, buffer[:8]) + for i := range r { + copy(out[(i+1)*8:], r[i]) + } + + return out, nil +} + +// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. +func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { + if len(ciphertext)%8 != 0 { + return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") + } + + n := (len(ciphertext) / 8) - 1 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], ciphertext[(i+1)*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer[:8], ciphertext[:8]) + + for t := 6*n - 1; t >= 0; t-- { + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] ^= tBytes[i] + } + copy(buffer[8:], r[t%n]) + + block.Decrypt(buffer, buffer) + + copy(r[t%n], buffer[8:]) + } + + if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { + return nil, errors.New("go-jose/go-jose: failed to unwrap key") + } + + out := make([]byte, n*8) + for i := range r { + copy(out[i*8:], r[i]) + } + + return out, nil +} diff --git a/vendor/github.com/go-jose/go-jose/v4/crypter.go b/vendor/github.com/go-jose/go-jose/v4/crypter.go new file mode 100644 index 0000000000..d81b03b447 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/crypter.go @@ -0,0 +1,599 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 jose + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "fmt" + + "github.com/go-jose/go-jose/v4/json" +) + +// Encrypter represents an encrypter which produces an encrypted JWE object. +type Encrypter interface { + Encrypt(plaintext []byte) (*JSONWebEncryption, error) + EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) + Options() EncrypterOptions +} + +// A generic content cipher +type contentCipher interface { + keySize() int + encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) + decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) +} + +// A key generator (for generating/getting a CEK) +type keyGenerator interface { + keySize() int + genKey() ([]byte, rawHeader, error) +} + +// A generic key encrypter +type keyEncrypter interface { + encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key +} + +// A generic key decrypter +type keyDecrypter interface { + decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key +} + +// A generic encrypter based on the given key encrypter and content cipher. +type genericEncrypter struct { + contentAlg ContentEncryption + compressionAlg CompressionAlgorithm + cipher contentCipher + recipients []recipientKeyInfo + keyGenerator keyGenerator + extraHeaders map[HeaderKey]interface{} +} + +type recipientKeyInfo struct { + keyID string + keyAlg KeyAlgorithm + keyEncrypter keyEncrypter +} + +// EncrypterOptions represents options that can be set on new encrypters. +type EncrypterOptions struct { + Compression CompressionAlgorithm + + // Optional map of name/value pairs to be inserted into the protected + // header of a JWS object. Some specifications which make use of + // JWS require additional values here. + // + // Values will be serialized by [json.Marshal] and must be valid inputs to + // that function. + // + // [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal + ExtraHeaders map[HeaderKey]interface{} +} + +// WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it +// if necessary, and returns the updated EncrypterOptions. +// +// The v parameter will be serialized by [json.Marshal] and must be a valid +// input to that function. +// +// [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal +func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { + if eo.ExtraHeaders == nil { + eo.ExtraHeaders = map[HeaderKey]interface{}{} + } + eo.ExtraHeaders[k] = v + return eo +} + +// WithContentType adds a content type ("cty") header and returns the updated +// EncrypterOptions. +func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderContentType, contentType) +} + +// WithType adds a type ("typ") header and returns the updated EncrypterOptions. +func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderType, typ) +} + +// Recipient represents an algorithm/key to encrypt messages to. +// +// PBES2Count and PBES2Salt correspond with the "p2c" and "p2s" headers used +// on the password-based encryption algorithms PBES2-HS256+A128KW, +// PBES2-HS384+A192KW, and PBES2-HS512+A256KW. If they are not provided a safe +// default of 100000 will be used for the count and a 128-bit random salt will +// be generated. +type Recipient struct { + Algorithm KeyAlgorithm + // Key must have one of these types: + // - ed25519.PublicKey + // - *ecdsa.PublicKey + // - *rsa.PublicKey + // - *JSONWebKey + // - JSONWebKey + // - []byte (a symmetric key) + // - Any type that satisfies the OpaqueKeyEncrypter interface + // + // The type of Key must match the value of Algorithm. + Key interface{} + KeyID string + PBES2Count int + PBES2Salt []byte +} + +// NewEncrypter creates an appropriate encrypter based on the key type +func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: getContentCipher(enc), + } + if opts != nil { + encrypter.compressionAlg = opts.Compression + encrypter.extraHeaders = opts.ExtraHeaders + } + + if encrypter.cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + + var keyID string + var rawKey interface{} + switch encryptionKey := rcpt.Key.(type) { + case JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + case *JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + case OpaqueKeyEncrypter: + keyID, rawKey = encryptionKey.KeyID(), encryptionKey + default: + rawKey = encryptionKey + } + + switch rcpt.Algorithm { + case DIRECT: + // Direct encryption mode must be treated differently + keyBytes, ok := rawKey.([]byte) + if !ok { + return nil, ErrUnsupportedKeyType + } + if encrypter.cipher.keySize() != len(keyBytes) { + return nil, ErrInvalidKeySize + } + encrypter.keyGenerator = staticKeyGenerator{ + key: keyBytes, + } + recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, keyBytes) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + case ECDH_ES: + // ECDH-ES (w/o key wrapping) is similar to DIRECT mode + keyDSA, ok := rawKey.(*ecdsa.PublicKey) + if !ok { + return nil, ErrUnsupportedKeyType + } + encrypter.keyGenerator = ecKeyGenerator{ + size: encrypter.cipher.keySize(), + algID: string(enc), + publicKey: keyDSA, + } + recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, keyDSA) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + default: + // Can just add a standard recipient + encrypter.keyGenerator = randomKeyGenerator{ + size: encrypter.cipher.keySize(), + } + err := encrypter.addRecipient(rcpt) + return encrypter, err + } +} + +// NewMultiEncrypter creates a multi-encrypter based on the given parameters +func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { + cipher := getContentCipher(enc) + + if cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + if len(rcpts) == 0 { + return nil, fmt.Errorf("go-jose/go-jose: recipients is nil or empty") + } + + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: cipher, + keyGenerator: randomKeyGenerator{ + size: cipher.keySize(), + }, + } + + if opts != nil { + encrypter.compressionAlg = opts.Compression + encrypter.extraHeaders = opts.ExtraHeaders + } + + for _, recipient := range rcpts { + err := encrypter.addRecipient(recipient) + if err != nil { + return nil, err + } + } + + return encrypter, nil +} + +func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { + var recipientInfo recipientKeyInfo + + switch recipient.Algorithm { + case DIRECT, ECDH_ES: + return fmt.Errorf("go-jose/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) + } + + recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) + if recipient.KeyID != "" { + recipientInfo.keyID = recipient.KeyID + } + + switch recipient.Algorithm { + case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW: + if sr, ok := recipientInfo.keyEncrypter.(*symmetricKeyCipher); ok { + sr.p2c = recipient.PBES2Count + sr.p2s = recipient.PBES2Salt + } + } + + if err == nil { + ctx.recipients = append(ctx.recipients, recipientInfo) + } + return err +} + +func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { + switch encryptionKey := encryptionKey.(type) { + case *rsa.PublicKey: + return newRSARecipient(alg, encryptionKey) + case *ecdsa.PublicKey: + return newECDHRecipient(alg, encryptionKey) + case []byte: + return newSymmetricRecipient(alg, encryptionKey) + case string: + return newSymmetricRecipient(alg, []byte(encryptionKey)) + case *JSONWebKey: + recipient, err := makeJWERecipient(alg, encryptionKey.Key) + recipient.keyID = encryptionKey.KeyID + return recipient, err + case OpaqueKeyEncrypter: + return newOpaqueKeyEncrypter(alg, encryptionKey) + } + return recipientKeyInfo{}, ErrUnsupportedKeyType +} + +// newDecrypter creates an appropriate decrypter based on the key type +func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { + switch decryptionKey := decryptionKey.(type) { + case *rsa.PrivateKey: + return &rsaDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case *ecdsa.PrivateKey: + return &ecDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case []byte: + return &symmetricKeyCipher{ + key: decryptionKey, + }, nil + case string: + return &symmetricKeyCipher{ + key: []byte(decryptionKey), + }, nil + case JSONWebKey: + return newDecrypter(decryptionKey.Key) + case *JSONWebKey: + return newDecrypter(decryptionKey.Key) + case OpaqueKeyDecrypter: + return &opaqueKeyDecrypter{decrypter: decryptionKey}, nil + default: + return nil, ErrUnsupportedKeyType + } +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { + return ctx.EncryptWithAuthData(plaintext, nil) +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { + obj := &JSONWebEncryption{} + obj.aad = aad + + obj.protected = &rawHeader{} + err := obj.protected.set(headerEncryption, ctx.contentAlg) + if err != nil { + return nil, err + } + + obj.recipients = make([]recipientInfo, len(ctx.recipients)) + + if len(ctx.recipients) == 0 { + return nil, fmt.Errorf("go-jose/go-jose: no recipients to encrypt to") + } + + cek, headers, err := ctx.keyGenerator.genKey() + if err != nil { + return nil, err + } + + obj.protected.merge(&headers) + + for i, info := range ctx.recipients { + recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) + if err != nil { + return nil, err + } + + err = recipient.header.set(headerAlgorithm, info.keyAlg) + if err != nil { + return nil, err + } + + if info.keyID != "" { + err = recipient.header.set(headerKeyID, info.keyID) + if err != nil { + return nil, err + } + } + obj.recipients[i] = recipient + } + + if len(ctx.recipients) == 1 { + // Move per-recipient headers into main protected header if there's + // only a single recipient. + obj.protected.merge(obj.recipients[0].header) + obj.recipients[0].header = nil + } + + if ctx.compressionAlg != NONE { + plaintext, err = compress(ctx.compressionAlg, plaintext) + if err != nil { + return nil, err + } + + err = obj.protected.set(headerCompression, ctx.compressionAlg) + if err != nil { + return nil, err + } + } + + for k, v := range ctx.extraHeaders { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + (*obj.protected)[k] = makeRawMessage(b) + } + + authData := obj.computeAuthData() + parts, err := ctx.cipher.encrypt(cek, authData, plaintext) + if err != nil { + return nil, err + } + + obj.iv = parts.iv + obj.ciphertext = parts.ciphertext + obj.tag = parts.tag + + return obj, nil +} + +func (ctx *genericEncrypter) Options() EncrypterOptions { + return EncrypterOptions{ + Compression: ctx.compressionAlg, + ExtraHeaders: ctx.extraHeaders, + } +} + +// Decrypt and validate the object and return the plaintext. This +// function does not support multi-recipient. If you desire multi-recipient +// decryption use DecryptMulti instead. +// +// The decryptionKey argument must contain a private or symmetric key +// and must have one of these types: +// - *ecdsa.PrivateKey +// - *rsa.PrivateKey +// - *JSONWebKey +// - JSONWebKey +// - *JSONWebKeySet +// - JSONWebKeySet +// - []byte (a symmetric key) +// - string (a symmetric key) +// - Any type that satisfies the OpaqueKeyDecrypter interface. +// +// Note that ed25519 is only available for signatures, not encryption, so is +// not an option here. +// +// Automatically decompresses plaintext, but returns an error if the decompressed +// data would be >250kB or >10x the size of the compressed data, whichever is larger. +func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { + headers := obj.mergedHeaders(nil) + + if len(obj.recipients) > 1 { + return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one") + } + + critical, err := headers.getCritical() + if err != nil { + return nil, fmt.Errorf("go-jose/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return nil, fmt.Errorf("go-jose/go-jose: unsupported crit header") + } + + key, err := tryJWKS(decryptionKey, obj.Header) + if err != nil { + return nil, err + } + decrypter, err := newDecrypter(key) + if err != nil { + return nil, err + } + + cipher := getContentCipher(headers.getEncryption()) + if cipher == nil { + return nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + var plaintext []byte + recipient := obj.recipients[0] + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + } + + if plaintext == nil { + return nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + if err != nil { + return nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) + } + } + + return plaintext, nil +} + +// DecryptMulti decrypts and validates the object and returns the plaintexts, +// with support for multiple recipients. It returns the index of the recipient +// for which the decryption was successful, the merged headers for that recipient, +// and the plaintext. +// +// The decryptionKey argument must have one of the types allowed for the +// decryptionKey argument of Decrypt(). +// +// Automatically decompresses plaintext, but returns an error if the decompressed +// data would be >250kB or >3x the size of the compressed data, whichever is larger. +func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { + globalHeaders := obj.mergedHeaders(nil) + + critical, err := globalHeaders.getCritical() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported crit header") + } + + key, err := tryJWKS(decryptionKey, obj.Header) + if err != nil { + return -1, Header{}, nil, err + } + decrypter, err := newDecrypter(key) + if err != nil { + return -1, Header{}, nil, err + } + + encryption := globalHeaders.getEncryption() + cipher := getContentCipher(encryption) + if cipher == nil { + return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(encryption)) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + index := -1 + var plaintext []byte + var headers rawHeader + + for i, recipient := range obj.recipients { + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + if err == nil { + index = i + headers = recipientHeaders + break + } + } + } + + if plaintext == nil { + return -1, Header{}, nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + if err != nil { + return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) + } + } + + sanitized, err := headers.sanitized() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to sanitize header: %v", err) + } + + return index, sanitized, plaintext, err +} diff --git a/vendor/github.com/go-jose/go-jose/v4/doc.go b/vendor/github.com/go-jose/go-jose/v4/doc.go new file mode 100644 index 0000000000..0ad40ca085 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/doc.go @@ -0,0 +1,25 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 jose aims to provide an implementation of the Javascript Object Signing +and Encryption set of standards. It implements encryption and signing based on +the JSON Web Encryption and JSON Web Signature standards, with optional JSON Web +Token support available in a sub-package. The library supports both the compact +and JWS/JWE JSON Serialization formats, and has optional support for multiple +recipients. +*/ +package jose diff --git a/vendor/github.com/go-jose/go-jose/v4/encoding.go b/vendor/github.com/go-jose/go-jose/v4/encoding.go new file mode 100644 index 0000000000..4f6e0d4a5c --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/encoding.go @@ -0,0 +1,228 @@ +/*- + * Copyright 2014 Square Inc. + * + * 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 jose + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "math/big" + "strings" + "unicode" + + "github.com/go-jose/go-jose/v4/json" +) + +// Helper function to serialize known-good objects. +// Precondition: value is not a nil pointer. +func mustSerializeJSON(value interface{}) []byte { + out, err := json.Marshal(value) + if err != nil { + panic(err) + } + // We never want to serialize the top-level value "null," since it's not a + // valid JOSE message. But if a caller passes in a nil pointer to this method, + // MarshalJSON will happily serialize it as the top-level value "null". If + // that value is then embedded in another operation, for instance by being + // base64-encoded and fed as input to a signing algorithm + // (https://github.com/go-jose/go-jose/issues/22), the result will be + // incorrect. Because this method is intended for known-good objects, and a nil + // pointer is not a known-good object, we are free to panic in this case. + // Note: It's not possible to directly check whether the data pointed at by an + // interface is a nil pointer, so we do this hacky workaround. + // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I + if string(out) == "null" { + panic("Tried to serialize a nil pointer.") + } + return out +} + +// Strip all newlines and whitespace +func stripWhitespace(data string) string { + buf := strings.Builder{} + buf.Grow(len(data)) + for _, r := range data { + if !unicode.IsSpace(r) { + buf.WriteRune(r) + } + } + return buf.String() +} + +// Perform compression based on algorithm +func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return deflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// Perform decompression based on algorithm +func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return inflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// deflate compresses the input. +func deflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + + // Writing to byte buffer, err is always nil + writer, _ := flate.NewWriter(output, 1) + _, _ = io.Copy(writer, bytes.NewBuffer(input)) + + err := writer.Close() + return output.Bytes(), err +} + +// inflate decompresses the input. +// +// Errors if the decompressed data would be >250kB or >10x the size of the +// compressed data, whichever is larger. +func inflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + reader := flate.NewReader(bytes.NewBuffer(input)) + + maxCompressedSize := max(250_000, 10*int64(len(input))) + + limit := maxCompressedSize + 1 + n, err := io.CopyN(output, reader, limit) + if err != nil && err != io.EOF { + return nil, err + } + if n == limit { + return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize) + } + + err = reader.Close() + return output.Bytes(), err +} + +// byteBuffer represents a slice of bytes that can be serialized to url-safe base64. +type byteBuffer struct { + data []byte +} + +func newBuffer(data []byte) *byteBuffer { + if data == nil { + return nil + } + return &byteBuffer{ + data: data, + } +} + +func newFixedSizeBuffer(data []byte, length int) *byteBuffer { + if len(data) > length { + panic("go-jose/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") + } + pad := make([]byte, length-len(data)) + return newBuffer(append(pad, data...)) +} + +func newBufferFromInt(num uint64) *byteBuffer { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, num) + return newBuffer(bytes.TrimLeft(data, "\x00")) +} + +func (b *byteBuffer) MarshalJSON() ([]byte, error) { + return json.Marshal(b.base64()) +} + +func (b *byteBuffer) UnmarshalJSON(data []byte) error { + var encoded string + err := json.Unmarshal(data, &encoded) + if err != nil { + return err + } + + if encoded == "" { + return nil + } + + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return err + } + + *b = *newBuffer(decoded) + + return nil +} + +func (b *byteBuffer) base64() string { + return base64.RawURLEncoding.EncodeToString(b.data) +} + +func (b *byteBuffer) bytes() []byte { + // Handling nil here allows us to transparently handle nil slices when serializing. + if b == nil { + return nil + } + return b.data +} + +func (b byteBuffer) bigInt() *big.Int { + return new(big.Int).SetBytes(b.data) +} + +func (b byteBuffer) toInt() int { + return int(b.bigInt().Int64()) +} + +func base64EncodeLen(sl []byte) int { + return base64.RawURLEncoding.EncodedLen(len(sl)) +} + +func base64JoinWithDots(inputs ...[]byte) string { + if len(inputs) == 0 { + return "" + } + + // Count of dots. + totalCount := len(inputs) - 1 + + for _, input := range inputs { + totalCount += base64EncodeLen(input) + } + + out := make([]byte, totalCount) + startEncode := 0 + for i, input := range inputs { + base64.RawURLEncoding.Encode(out[startEncode:], input) + + if i == len(inputs)-1 { + continue + } + + startEncode += base64EncodeLen(input) + out[startEncode] = '.' + startEncode++ + } + + return string(out) +} diff --git a/vendor/github.com/go-jose/go-jose/v4/json/LICENSE b/vendor/github.com/go-jose/go-jose/v4/json/LICENSE new file mode 100644 index 0000000000..7448756763 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/json/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/go-jose/go-jose/v4/json/README.md b/vendor/github.com/go-jose/go-jose/v4/json/README.md new file mode 100644 index 0000000000..86de5e5581 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/json/README.md @@ -0,0 +1,13 @@ +# Safe JSON + +This repository contains a fork of the `encoding/json` package from Go 1.6. + +The following changes were made: + +* Object deserialization uses case-sensitive member name matching instead of + [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html). + This is to avoid differences in the interpretation of JOSE messages between + go-jose and libraries written in other languages. +* When deserializing a JSON object, we check for duplicate keys and reject the + input whenever we detect a duplicate. Rather than trying to work with malformed + data, we prefer to reject it right away. diff --git a/vendor/github.com/go-jose/go-jose/v4/json/decode.go b/vendor/github.com/go-jose/go-jose/v4/json/decode.go new file mode 100644 index 0000000000..50634dd847 --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/json/decode.go @@ -0,0 +1,1216 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "errors" + "fmt" + "math" + "reflect" + "runtime" + "strconv" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. +// Unmarshal will only set exported fields of the struct. +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a string-keyed map, Unmarshal first +// establishes a map to use, If the map is nil, Unmarshal allocates a new map. +// Otherwise Unmarshal reuses the existing map, keeping existing entries. +// Unmarshal then stores key-value pairs from the JSON object into the map. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// “not present,” unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +func Unmarshal(data []byte, v interface{}) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by objects +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes +} + +func (e *UnmarshalTypeError) Error() string { + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// (No longer used; kept for compatibility.) +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + err = r.(error) + } + }() + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + d.value(rv) + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// isValidNumber reports whether s is a valid JSON number literal. +func isValidNumber(s string) bool { + // This function implements the JSON numbers grammar. + // See https://tools.ietf.org/html/rfc7159#section-6 + // and http://json.org/number.gif + + if s == "" { + return false + } + + // Optional - + if s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + + // Digits + switch { + default: + return false + + case s[0] == '0': + s = s[1:] + + case '1' <= s[0] && s[0] <= '9': + s = s[1:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // . followed by 1 or more digits. + if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { + s = s[2:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // e or E followed by an optional - or + and + // 1 or more digits. + if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { + s = s[1:] + if s[0] == '+' || s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // Make sure we are at the end. + return s == "" +} + +type NumberUnmarshalType int + +const ( + // unmarshal a JSON number into an interface{} as a float64 + UnmarshalFloat NumberUnmarshalType = iota + // unmarshal a JSON number into an interface{} as a `json.Number` + UnmarshalJSONNumber + // unmarshal a JSON number into an interface{} as a int64 + // if value is an integer otherwise float64 + UnmarshalIntOrFloat +) + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // read offset in data + scan scanner + nextscan scanner // for calls to nextValue + savedError error + numberType NumberUnmarshalType +} + +// errPhase is used for errors that should not happen unless +// there is a bug in the JSON decoder or something is editing +// the data slice while the decoder executes. +var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + return d +} + +// error aborts the decoding by panicking with err. +func (d *decodeState) error(err error) { + panic(err) +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = err + } +} + +// next cuts off and returns the next full JSON value in d.data[d.off:]. +// The next value is known to be an object or array, not a literal. +func (d *decodeState) next() []byte { + c := d.data[d.off] + item, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // Our scanner has seen the opening brace/bracket + // and thinks we're still in the middle of the object. + // invent a closing brace/bracket to get it out. + if c == '{' { + d.scan.step(&d.scan, '}') + } else { + d.scan.step(&d.scan, ']') + } + + return item +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +// It updates d.off and returns the new scan code. +func (d *decodeState) scanWhile(op int) int { + var newOp int + for { + if d.off >= len(d.data) { + newOp = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } else { + c := d.data[d.off] + d.off++ + newOp = d.scan.step(&d.scan, c) + } + if newOp != op { + break + } + } + return newOp +} + +// value decodes a JSON value from d.data[d.off:] into the value. +// it updates d.off to point past the decoded value. +func (d *decodeState) value(v reflect.Value) { + if !v.IsValid() { + _, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // d.scan thinks we're still at the beginning of the item. + // Feed in an empty string - the shortest, simplest value - + // so that it knows we got to the end of the value. + if d.scan.redo { + // rewind. + d.scan.redo = false + d.scan.step = stateBeginValue + } + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + + n := len(d.scan.parseState) + if n > 0 && d.scan.parseState[n-1] == parseObjectKey { + // d.scan thinks we just read an object key; finish the object + d.scan.step(&d.scan, ':') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '}') + } + + return + } + + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(v) + + case scanBeginObject: + d.object(v) + + case scanBeginLiteral: + d.literal(v) + } +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() interface{} { + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(reflect.Value{}) + + case scanBeginObject: + d.object(reflect.Value{}) + + case scanBeginLiteral: + switch v := d.literalInterface().(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// if it encounters an Unmarshaler, indirect stops and returns that. +// if decodingNull is true, indirect stops at the last pointer so it can be set to nil. +func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + v = e + continue + } + } + + if v.Kind() != reflect.Ptr { + break + } + + if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + v = v.Elem() + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into the value v. +// the first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + } + + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + v.Set(reflect.ValueOf(d.arrayInterface())) + return + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + case reflect.Array: + case reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + d.value(v.Index(i)) + } else { + // Ran out of fixed array: skip. + d.value(reflect.Value{}) + } + i++ + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } +} + +var nullLiteral = []byte("null") + +// object consumes an object from d.data[d.off-1:], decoding into the value v. +// the first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + v = pv + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + v.Set(reflect.ValueOf(d.objectInterface())) + return + } + + // Check type of target: struct or map[string]T + switch v.Kind() { + case reflect.Map: + // map must have string kind + t := v.Type() + if t.Key().Kind() != reflect.String { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + + default: + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + + var mapElem reflect.Value + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := v.Type().Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.Set(reflect.Zero(elemType)) + } + subv = mapElem + } else { + var f *field + fields := cachedTypeFields(v.Type()) + for i := range fields { + ff := &fields[i] + if bytes.Equal(ff.nameBytes, []byte(key)) { + f = ff + break + } + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Ptr { + if subv.IsNil() { + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + } + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + d.literalStore(nullLiteral, subv, false) + case string: + d.literalStore([]byte(qv), subv, true) + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + d.value(subv) + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kv := reflect.ValueOf(key).Convert(v.Type().Key()) + v.SetMapIndex(kv, subv) + } + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } +} + +// literal consumes a literal from d.data[d.off-1:], decoding into the value v. +// The first byte of the literal has been read already +// (that's how the caller knows it's a literal). +func (d *decodeState) literal(v reflect.Value) { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + + d.literalStore(d.data[start:d.off], v, false) +} + +// convertNumber converts the number literal s to a float64, int64 or a Number +// depending on d.numberDecodeType. +func (d *decodeState) convertNumber(s string) (interface{}, error) { + switch d.numberType { + + case UnmarshalJSONNumber: + return Number(s), nil + case UnmarshalIntOrFloat: + v, err := strconv.ParseInt(s, 10, 64) + if err == nil { + return v, nil + } + + // tries to parse integer number in scientific notation + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} + } + + // if it has no decimal value use int64 + if fi, fd := math.Modf(f); fd == 0.0 { + return int64(fi), nil + } + return f, nil + default: + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} + } + return f, nil + } + +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { + // Check for unmarshaler. + if len(item) == 0 { + //Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return + } + wantptr := item[0] == 'n' // null + u, ut, pv := d.indirect(v, wantptr) + if u != nil { + err := u.UnmarshalJSON(item) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + return + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + err := ut.UnmarshalText(s) + if err != nil { + d.error(err) + } + return + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + switch v.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + v.Set(reflect.Zero(v.Type())) + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := c == 't' + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + v.SetString(s) + if !isValidNumber(s) { + d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) + } + break + } + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + } + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetFloat(n) + } + } +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() interface{} { + switch d.scanWhile(scanSkipSpace) { + default: + d.error(errPhase) + panic("unreachable") + case scanBeginArray: + return d.arrayInterface() + case scanBeginObject: + return d.objectInterface() + case scanBeginLiteral: + return d.literalInterface() + } +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []interface{} { + var v = make([]interface{}, 0) + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]interface{} { + m := make(map[string]interface{}) + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read string key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } + return m +} + +// literalInterface is like literal but returns an interface value. +func (d *decodeState) literalInterface() interface{} { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + item := d.data[start:d.off] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + d.error(errPhase) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + d.error(errPhase) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + r, err := strconv.ParseUint(string(s[2:6]), 16, 64) + if err != nil { + return -1 + } + return rune(r) +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/vendor/github.com/go-jose/go-jose/v4/json/encode.go b/vendor/github.com/go-jose/go-jose/v4/json/encode.go new file mode 100644 index 0000000000..98de68ce1e --- /dev/null +++ b/vendor/github.com/go-jose/go-jose/v4/json/encode.go @@ -0,0 +1,1197 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON objects as defined in +// RFC 4627. The mapping between JSON objects and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" +// to keep some browsers from misinterpreting JSON output as HTML. +// Ampersand "&" is also escaped to "\u0026" for the same reason. +// +// Array and slice values encode as JSON arrays, except that +// []byte encodes as a base64-encoded string, and a nil slice +// encodes as the null JSON object. +// +// Struct values encode as JSON objects. Each exported struct field +// becomes a member of the object unless +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option. +// +// The empty values are false, 0, any +// nil pointer or interface value, and any array, slice, map, or string of +// length zero. The object's default key string is the struct field name +// but can be specified in the struct field's tag value. The "json" key in +// the struct field's tag value is the key name, followed by an optional comma +// and options. Examples: +// +// // Field is ignored by this package. +// Field int `json:"-"` +// +// // Field appears in JSON as key "myName". +// Field int `json:"myName"` +// +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is empty, +// // as defined above. +// Field int `json:"myName,omitempty"` +// +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if empty. +// // Note the leading comma. +// Field int `json:",omitempty"` +// +// The "string" option signals that a field is stored as JSON inside a +// JSON-encoded string. It applies only to fields of string, floating point, +// integer, or boolean types. This extra level of encoding is sometimes used +// when communicating with JavaScript programs: +// +// Int64String int64 `json:",string"` +// +// The key name will be used if it's a non-empty string consisting of +// only Unicode letters, digits, dollar signs, percent signs, hyphens, +// underscores and slashes. +// +// Anonymous struct fields are usually marshaled as if their inner exported fields +// were fields in the outer struct, subject to the usual Go visibility rules amended +// as described in the next paragraph. +// An anonymous struct field with a name given in its JSON tag is treated as +// having that name, rather than being anonymous. +// An anonymous struct field of interface type is treated the same as having +// that type as its name, rather than being anonymous. +// +// The Go visibility rules for struct fields are amended for JSON when +// deciding which field to marshal or unmarshal. If there are +// multiple fields at the same level, and that level is the least +// nested (and would therefore be the nesting level selected by the +// usual Go rules), the following extra rules apply: +// +// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, +// even if there are multiple untagged fields that would otherwise conflict. +// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. +// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. +// +// Handling of anonymous struct fields is new in Go 1.1. +// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of +// an anonymous struct field in both current and earlier versions, give the field +// a JSON tag of "-". +// +// Map values encode as JSON objects. +// The map's key type must be string; the map keys are used as JSON object +// keys, subject to the UTF-8 coercion described for string values above. +// +// Pointer values encode as the value pointed to. +// A nil pointer encodes as the null JSON object. +// +// Interface values encode as the value contained in the interface. +// A nil interface value encodes as the null JSON object. +// +// Channel, complex, and function values cannot be encoded in JSON. +// Attempting to encode such a value causes Marshal to return +// an UnsupportedTypeError. +// +// JSON cannot represent cyclic data structures and Marshal does not +// handle them. Passing cyclic structures to Marshal will result in +// an infinite recursion. +func Marshal(v interface{}) ([]byte, error) { + e := &encodeState{} + err := e.marshal(v) + if err != nil { + return nil, err + } + return e.Bytes(), nil +} + +// MarshalIndent is like Marshal but applies Indent to format the output. +func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { + b, err := Marshal(v) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = Indent(&buf, b, prefix, indent) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 +// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 +// so that the JSON will be safe to embed inside HTML