Skip to content

Implement SpinApp per-request invocation limits #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1alpha1/spinapp_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ type SpinAppSpec struct {
// If not specified, the default service account will be used.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// InvocationLimits define limits to be applied per invocation of the app.
// The keys are the names of the limits and the values are the limit values.
// SpinKube executors may define their own limits.
InvocationLimits map[string]string `json:"invocationLimits,omitempty"`
}

// SpinAppStatus defines the observed state of SpinApp
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 54 additions & 24 deletions config/crd/bases/core.spinkube.dev_spinapps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ spec:
type: object
x-kubernetes-map-type: atomic
type: array
invocationLimits:
additionalProperties:
type: string
description: |-
InvocationLimits define limits to be applied per invocation of the app.
The keys are the names of the limits and the values are the limit values.
SpinKube executors may define their own limits.
type: object
podAnnotations:
additionalProperties:
type: string
Expand Down Expand Up @@ -728,6 +736,8 @@ spec:
description: |-
awsElasticBlockStore represents an AWS Disk resource that is attached to a
kubelet's host machine and then exposed to the pod.
Deprecated: AWSElasticBlockStore is deprecated. All operations for the in-tree
awsElasticBlockStore type are redirected to the ebs.csi.aws.com CSI driver.
More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
properties:
fsType:
Expand Down Expand Up @@ -759,8 +769,10 @@ spec:
- volumeID
type: object
azureDisk:
description: azureDisk represents an Azure Data Disk mount on
the host and bind mount to the pod.
description: |-
azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.
Deprecated: AzureDisk is deprecated. All operations for the in-tree azureDisk type
are redirected to the disk.csi.azure.com CSI driver.
properties:
cachingMode:
description: 'cachingMode is the Host Caching mode: None,
Expand Down Expand Up @@ -798,8 +810,10 @@ spec:
- diskURI
type: object
azureFile:
description: azureFile represents an Azure File Service mount
on the host and bind mount to the pod.
description: |-
azureFile represents an Azure File Service mount on the host and bind mount to the pod.
Deprecated: AzureFile is deprecated. All operations for the in-tree azureFile type
are redirected to the file.csi.azure.com CSI driver.
properties:
readOnly:
description: |-
Expand All @@ -818,8 +832,9 @@ spec:
- shareName
type: object
cephfs:
description: cephFS represents a Ceph FS mount on the host that
shares a pod's lifetime
description: |-
cephFS represents a Ceph FS mount on the host that shares a pod's lifetime.
Deprecated: CephFS is deprecated and the in-tree cephfs type is no longer supported.
properties:
monitors:
description: |-
Expand Down Expand Up @@ -871,6 +886,8 @@ spec:
cinder:
description: |-
cinder represents a cinder volume attached and mounted on kubelets host machine.
Deprecated: Cinder is deprecated. All operations for the in-tree cinder type
are redirected to the cinder.csi.openstack.org CSI driver.
More info: https://examples.k8s.io/mysql-cinder-pd/README.md
properties:
fsType:
Expand Down Expand Up @@ -980,8 +997,7 @@ spec:
x-kubernetes-map-type: atomic
csi:
description: csi (Container Storage Interface) represents ephemeral
storage that is handled by certain external CSI drivers (Beta
feature).
storage that is handled by certain external CSI drivers.
properties:
driver:
description: |-
Expand Down Expand Up @@ -1447,6 +1463,7 @@ spec:
description: |-
flexVolume represents a generic volume resource that is
provisioned/attached using an exec based plugin.
Deprecated: FlexVolume is deprecated. Consider using a CSIDriver instead.
properties:
driver:
description: driver is the name of the driver to use for
Expand Down Expand Up @@ -1492,9 +1509,9 @@ spec:
- driver
type: object
flocker:
description: flocker represents a Flocker volume attached to
a kubelet's host machine. This depends on the Flocker control
service being running
description: |-
flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running.
Deprecated: Flocker is deprecated and the in-tree flocker type is no longer supported.
properties:
datasetName:
description: |-
Expand All @@ -1510,6 +1527,8 @@ spec:
description: |-
gcePersistentDisk represents a GCE Disk resource that is attached to a
kubelet's host machine and then exposed to the pod.
Deprecated: GCEPersistentDisk is deprecated. All operations for the in-tree
gcePersistentDisk type are redirected to the pd.csi.storage.gke.io CSI driver.
More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
properties:
fsType:
Expand Down Expand Up @@ -1545,7 +1564,7 @@ spec:
gitRepo:
description: |-
gitRepo represents a git repository at a particular revision.
DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an
Deprecated: GitRepo is deprecated. To provision a container with a git repo, mount an
EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir
into the Pod's container.
properties:
Expand All @@ -1569,6 +1588,7 @@ spec:
glusterfs:
description: |-
glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime.
Deprecated: Glusterfs is deprecated and the in-tree glusterfs type is no longer supported.
More info: https://examples.k8s.io/volumes/glusterfs/README.md
properties:
endpoints:
Expand Down Expand Up @@ -1778,8 +1798,9 @@ spec:
- claimName
type: object
photonPersistentDisk:
description: photonPersistentDisk represents a PhotonController
persistent disk attached and mounted on kubelets host machine
description: |-
photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine.
Deprecated: PhotonPersistentDisk is deprecated and the in-tree photonPersistentDisk type is no longer supported.
properties:
fsType:
description: |-
Expand All @@ -1795,8 +1816,11 @@ spec:
- pdID
type: object
portworxVolume:
description: portworxVolume represents a portworx volume attached
and mounted on kubelets host machine
description: |-
portworxVolume represents a portworx volume attached and mounted on kubelets host machine.
Deprecated: PortworxVolume is deprecated. All operations for the in-tree portworxVolume type
are redirected to the pxd.portworx.com CSI driver when the CSIMigrationPortworx feature-gate
is on.
properties:
fsType:
description: |-
Expand Down Expand Up @@ -2161,8 +2185,9 @@ spec:
x-kubernetes-list-type: atomic
type: object
quobyte:
description: quobyte represents a Quobyte mount on the host
that shares a pod's lifetime
description: |-
quobyte represents a Quobyte mount on the host that shares a pod's lifetime.
Deprecated: Quobyte is deprecated and the in-tree quobyte type is no longer supported.
properties:
group:
description: |-
Expand Down Expand Up @@ -2201,6 +2226,7 @@ spec:
rbd:
description: |-
rbd represents a Rados Block Device mount on the host that shares a pod's lifetime.
Deprecated: RBD is deprecated and the in-tree rbd type is no longer supported.
More info: https://examples.k8s.io/volumes/rbd/README.md
properties:
fsType:
Expand Down Expand Up @@ -2273,8 +2299,9 @@ spec:
- monitors
type: object
scaleIO:
description: scaleIO represents a ScaleIO persistent volume
attached and mounted on Kubernetes nodes.
description: |-
scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes.
Deprecated: ScaleIO is deprecated and the in-tree scaleIO type is no longer supported.
properties:
fsType:
default: xfs
Expand Down Expand Up @@ -2406,8 +2433,9 @@ spec:
type: string
type: object
storageos:
description: storageOS represents a StorageOS volume attached
and mounted on Kubernetes nodes.
description: |-
storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes.
Deprecated: StorageOS is deprecated and the in-tree storageos type is no longer supported.
properties:
fsType:
description: |-
Expand Down Expand Up @@ -2452,8 +2480,10 @@ spec:
type: string
type: object
vsphereVolume:
description: vsphereVolume represents a vSphere volume attached
and mounted on kubelets host machine
description: |-
vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine.
Deprecated: VsphereVolume is deprecated. All operations for the in-tree vsphereVolume type
are redirected to the csi.vsphere.vmware.com CSI driver.
properties:
fsType:
description: |-
Expand Down
20 changes: 18 additions & 2 deletions internal/controller/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/intstr"

spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1"
Expand Down Expand Up @@ -103,7 +105,7 @@ func ConstructVolumeMountsForApp(ctx context.Context, app *spinv1alpha1.SpinApp,

// ConstructEnvForApp constructs the env for a spin app that runs as a k8s pod.
// Variables are not guaranteed to stay backed by ENV.
func ConstructEnvForApp(ctx context.Context, app *spinv1alpha1.SpinApp, listenPort int, otel *spinv1alpha1.OtelConfig) []corev1.EnvVar {
func ConstructEnvForApp(ctx context.Context, app *spinv1alpha1.SpinApp, listenPort int, otel *spinv1alpha1.OtelConfig) ([]corev1.EnvVar, error) {
envs := make([]corev1.EnvVar, len(app.Spec.Variables))
// Adding the Spin Variables
for idx, variable := range app.Spec.Variables {
Expand Down Expand Up @@ -141,7 +143,21 @@ func ConstructEnvForApp(ctx context.Context, app *spinv1alpha1.SpinApp, listenPo
}
}

return envs
// Set known invocation limits as environment variables.
if limit, exists := app.Spec.InvocationLimits["memory"]; exists {
// Convert limit to bytes
quantity, err := resource.ParseQuantity(limit)
if err != nil {
return nil, fmt.Errorf("failed to parse memory limit %q: %w", limit, err)
}
bytes := quantity.ToDec().Value()
envs = append(envs, corev1.EnvVar{
Name: "SPIN_MAX_INSTANCE_MEMORY",
Value: strconv.Itoa(int(bytes)),
})
}

return envs, nil
}

func SpinHealthCheckToCoreProbe(probe *spinv1alpha1.HealthProbe) (*corev1.Probe, error) {
Expand Down
44 changes: 30 additions & 14 deletions internal/controller/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ func TestConstructEnvForApp(t *testing.T) {
value string
valueFrom *corev1.EnvVarSource

expectedOtelVars map[string]string
expectedVars map[string]string
otelVars spinv1alpha1.OtelConfig
invocationLimits map[string]string
}{
{
name: "simple_secret_with_static_value",
Expand Down Expand Up @@ -158,13 +159,25 @@ func TestConstructEnvForApp(t *testing.T) {
ExporterOtlpMetricsEndpoint: "http://metrics",
ExporterOtlpLogsEndpoint: "http://logs",
},
expectedOtelVars: map[string]string{
expectedVars: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://otlp",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs",
},
},
{
name: "memory_invocation_limit",
varName: "simple_secret",
expectedEnvName: "SPIN_VARIABLE_SIMPLE_SECRET",
value: "f00",
invocationLimits: map[string]string{
"memory": "40M",
},
expectedVars: map[string]string{
"SPIN_MAX_INSTANCE_MEMORY": "40000000", // 40M in bytes
},
},
}

for _, test := range tests {
Expand All @@ -177,29 +190,32 @@ func TestConstructEnvForApp(t *testing.T) {
ValueFrom: test.valueFrom,
},
}
app.Spec.InvocationLimits = test.invocationLimits

envs := ConstructEnvForApp(context.Background(), app, 0, &test.otelVars)
envs, err := ConstructEnvForApp(context.Background(), app, 0, &test.otelVars)
require.NoError(t, err)

require.Equal(t, test.expectedEnvName, envs[0].Name)
require.Equal(t, test.value, envs[0].Value)
require.Equal(t, test.valueFrom, envs[0].ValueFrom)

for key, value := range test.expectedOtelVars {
varNotFound := true
for _, envVar := range envs {
if envVar.Name == key {
varNotFound = false
if envVar.Value != value {
require.Equal(t, test.value, envVar.Value)
inMap := func(expected map[string]string, envs []corev1.EnvVar) {
for key, value := range expected {
varNotFound := true
for _, envVar := range envs {
if envVar.Name == key {
varNotFound = false
require.Equal(t, value, envVar.Value)
break
}
}
}

if varNotFound {
require.NotContains(t, test.expectedOtelVars, key)
if varNotFound {
require.NotContains(t, expected, key)
}
}
}

inMap(test.expectedVars, envs)
})
}
}
Expand Down
6 changes: 5 additions & 1 deletion internal/controller/spinapp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,11 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config
Requests: app.Spec.Resources.Requests,
}

env := ConstructEnvForApp(ctx, app, spinapp.DefaultHTTPPort, config.Otel)
env, err := ConstructEnvForApp(ctx, app, spinapp.DefaultHTTPPort, config.Otel)
if err != nil {
return nil, err
}

if app.Spec.Components != nil {
env = append(env, corev1.EnvVar{
Name: "SPIN_COMPONENTS_TO_RETAIN",
Expand Down
Loading
Loading