From 9801e5dfe9e17fc6c30ef832d97439955964fdcc Mon Sep 17 00:00:00 2001 From: RealAnna <89971034+RealAnna@users.noreply.github.com> Date: Tue, 30 May 2023 11:47:04 +0200 Subject: [PATCH 1/4] fix(metrics-operator): improve error handling in metrics providers (#1466) Signed-off-by: realanna Signed-off-by: RealAnna <89971034+RealAnna@users.noreply.github.com> Co-authored-by: Giovanni Liva Co-authored-by: odubajDT <93584209+odubajDT@users.noreply.github.com> --- .../common/providers/datadog/datadog.go | 10 +- .../common/providers/datadog/datadog_test.go | 125 +++++++++-------- .../common/providers/dynatrace/common.go | 7 + .../common/providers/dynatrace/dynatrace.go | 15 +- .../providers/dynatrace/dynatrace_dql.go | 9 ++ .../providers/dynatrace/dynatrace_dql_test.go | 52 ++++++- .../providers/dynatrace/dynatrace_test.go | 131 +++++++++--------- .../common/providers/provider_test.go | 6 + .../controllers/metrics/controller.go | 11 +- 9 files changed, 228 insertions(+), 138 deletions(-) diff --git a/metrics-operator/controllers/common/providers/datadog/datadog.go b/metrics-operator/controllers/common/providers/datadog/datadog.go index 54c8bd20815..19a873e45cb 100644 --- a/metrics-operator/controllers/common/providers/datadog/datadog.go +++ b/metrics-operator/controllers/common/providers/datadog/datadog.go @@ -65,7 +65,13 @@ func (d *KeptnDataDogProvider) EvaluateQuery(ctx context.Context, metric metrics err = json.Unmarshal(b, &result) if err != nil { d.Log.Error(err, "Error while parsing response") - return "", nil, err + return "", b, err + } + + if result.Error != nil { + err = fmt.Errorf("%s", *result.Error) + d.Log.Error(err, "Error from DataDog provider") + return "", b, err } if len(result.Series) == 0 { @@ -76,7 +82,7 @@ func (d *KeptnDataDogProvider) EvaluateQuery(ctx context.Context, metric metrics points := (result.Series)[0].Pointlist if len(points) == 0 { d.Log.Info("No metric points in query result") - return "", nil, fmt.Errorf("no metric points in query result") + return "", b, fmt.Errorf("no metric points in query result") } r := d.getSingleValue(points) diff --git a/metrics-operator/controllers/common/providers/datadog/datadog_test.go b/metrics-operator/controllers/common/providers/datadog/datadog_test.go index 2c426bf5b46..7f4d9d9f855 100644 --- a/metrics-operator/controllers/common/providers/datadog/datadog_test.go +++ b/metrics-operator/controllers/common/providers/datadog/datadog_test.go @@ -18,14 +18,16 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) +const ddErrorPayload = "{\"error\":\"Token is missing required scope\"}" const ddPayload = "{\"from_date\":1677736306000,\"group_by\":[],\"message\":\"\",\"query\":\"system.cpu.idle{*}\",\"res_type\":\"time_series\",\"series\":[{\"aggr\":null,\"display_name\":\"system.cpu.idle\",\"end\":1677821999000,\"expression\":\"system.cpu.idle{*}\",\"interval\":300,\"length\":7,\"metric\":\"system.cpu.idle\",\"pointlist\":[[1677781200000,92.37997436523438],[1677781500000,91.46615447998047],[1677781800000,92.05865631103515],[1677782100000,97.49858474731445],[1677782400000,95.95263163248698],[1677821400000,69.67094268798829],[1677821700000,84.78184509277344]],\"query_index\":0,\"scope\":\"*\",\"start\":1677781200000,\"tag_set\":[],\"unit\":[{\"family\":\"percentage\",\"name\":\"percent\",\"plural\":\"percent\",\"scale_factor\":1,\"short_name\":\"%\"},{}]}],\"status\":\"ok\",\"to_date\":1677822706000}" const ddEmptyPayload = "{\"from_date\":1677736306000,\"group_by\":[],\"message\":\"\",\"query\":\"system.cpu.idle{*}\",\"res_type\":\"time_series\",\"series\":[],\"status\":\"ok\",\"to_date\":1677822706000}" -func TestEvaluateQuery_HappyPath(t *testing.T) { +func TestEvaluateQuery_APIError(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(ddPayload)) + _, err := w.Write([]byte(ddErrorPayload)) require.Nil(t, err) })) defer svr.Close() @@ -43,13 +45,53 @@ func TestEvaluateQuery_HappyPath(t *testing.T) { appKey: []byte(appKeyValue), }, } - fakeClient := fake.NewClient(apiToken) + kdd := setupTest(apiToken) + metric := metricsapi.KeptnMetric{ + Spec: metricsapi.KeptnMetricSpec{ + Query: "system.cpu.idle{*}", + }, + } + b := true + p := metricsapi.KeptnMetricsProvider{ + Spec: metricsapi.KeptnMetricsProviderSpec{ + SecretKeyRef: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: secretName, + }, + Optional: &b, + }, + TargetServer: svr.URL, + }, + } - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, + r, raw, e := kdd.EvaluateQuery(context.TODO(), metric, p) + require.Error(t, e) + require.Contains(t, e.Error(), "Token is missing required scope") + require.Equal(t, []byte(ddErrorPayload), raw) + require.Empty(t, r) +} + +func TestEvaluateQuery_HappyPath(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(ddPayload)) + require.Nil(t, err) + })) + defer svr.Close() + + secretName := "datadogSecret" + apiKey, apiKeyValue := "DD_CLIENT_API_KEY", "fake-api-key" + appKey, appKeyValue := "DD_CLIENT_APP_KEY", "fake-app-key" + apiToken := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "", + }, + Data: map[string][]byte{ + apiKey: []byte(apiKeyValue), + appKey: []byte(appKeyValue), + }, } + kdd := setupTest(apiToken) metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -93,13 +135,7 @@ func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { appKey: []byte(appKeyValue), }, } - fakeClient := fake.NewClient(apiToken) - - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest(apiToken) metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -120,7 +156,7 @@ func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { r, raw, e := kdd.EvaluateQuery(context.TODO(), metric, p) require.Equal(t, "", r) - require.Equal(t, []byte(nil), raw) + require.Equal(t, []byte("garbage"), raw) require.NotNil(t, e) } func TestEvaluateQuery_MissingSecret(t *testing.T) { @@ -130,13 +166,7 @@ func TestEvaluateQuery_MissingSecret(t *testing.T) { })) defer svr.Close() - fakeClient := fake.NewClient() - - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest() metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -159,14 +189,9 @@ func TestEvaluateQuery_SecretNotFound(t *testing.T) { })) defer svr.Close() - fakeClient := fake.NewClient() secretName := "datadogSecret" - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest() metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -207,13 +232,7 @@ func TestEvaluateQuery_RefNonExistingKey(t *testing.T) { apiKey: []byte(apiKeyValue), }, } - fakeClient := fake.NewClient(apiToken) - - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest(apiToken) metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -256,13 +275,7 @@ func TestEvaluateQuery_EmptyPayload(t *testing.T) { appKey: []byte(appKeyValue), }, } - fakeClient := fake.NewClient(apiToken) - - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest(apiToken) metric := metricsapi.KeptnMetric{ Spec: metricsapi.KeptnMetricSpec{ Query: "system.cpu.idle{*}", @@ -282,30 +295,22 @@ func TestEvaluateQuery_EmptyPayload(t *testing.T) { } r, raw, e := kdd.EvaluateQuery(context.TODO(), metric, p) + t.Log(string(raw)) require.Nil(t, raw) require.Equal(t, "", r) require.True(t, strings.Contains(e.Error(), "no values in query result")) } func TestGetSingleValue_EmptyPoints(t *testing.T) { - fakeClient := fake.NewClient() - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + kdd := setupTest() var points [][]*float64 value := kdd.getSingleValue(points) require.Zero(t, value) } func TestGetSingleValue_HappyPath(t *testing.T) { - fakeClient := fake.NewClient() - kdd := KeptnDataDogProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } + + kdd := setupTest() result := datadogV1.MetricsQueryResponse{} _ = json.Unmarshal([]byte(ddPayload), &result) points := (result.Series)[0].Pointlist @@ -314,3 +319,15 @@ func TestGetSingleValue_HappyPath(t *testing.T) { require.NotZero(t, value) require.Equal(t, 89.11554133097331, value) } + +func setupTest(objs ...client.Object) KeptnDataDogProvider { + + fakeClient := fake.NewClient(objs...) + + kdd := KeptnDataDogProvider{ + HttpClient: http.Client{}, + Log: ctrl.Log.WithName("testytest"), + K8sClient: fakeClient, + } + return kdd +} diff --git a/metrics-operator/controllers/common/providers/dynatrace/common.go b/metrics-operator/controllers/common/providers/dynatrace/common.go index 23ba4dab368..bded16027cc 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/common.go +++ b/metrics-operator/controllers/common/providers/dynatrace/common.go @@ -15,6 +15,13 @@ var ErrSecretKeyRefNotDefined = errors.New("the SecretKeyRef property with the D var ErrInvalidResult = errors.New("the answer does not contain any data") var ErrDQLQueryTimeout = errors.New("timed out waiting for result of DQL query") +const ErrAPIMsg = "provider api response: %s" + +type Error struct { + Code int `json:"-"` // optional + Message string `json:"message"` +} + func getDTSecret(ctx context.Context, provider metricsapi.KeptnMetricsProvider, k8sClient client.Client) (string, error) { if !provider.HasSecretDefined() { return "", ErrSecretKeyRefNotDefined diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go index 5b95c8a8177..5b819577d5b 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + "net/url" + "reflect" "strings" "time" @@ -24,6 +26,7 @@ type DynatraceResponse struct { TotalCount int `json:"totalCount"` Resolution string `json:"resolution"` Result []DynatraceResult `json:"result"` + Error `json:"error"` } type DynatraceResult struct { @@ -39,7 +42,8 @@ type DynatraceData struct { // EvaluateQuery fetches the SLI values from dynatrace provider func (d *KeptnDynatraceProvider) EvaluateQuery(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) (string, []byte, error) { baseURL := d.normalizeAPIURL(provider.Spec.TargetServer) - qURL := baseURL + "v2/metrics/query?metricSelector=" + metric.Spec.Query + query := url.QueryEscape(metric.Spec.Query) + qURL := baseURL + "v2/metrics/query?metricSelector=" + query d.Log.Info("Running query: " + qURL) ctx, cancel := context.WithTimeout(ctx, 20*time.Second) @@ -57,6 +61,7 @@ func (d *KeptnDynatraceProvider) EvaluateQuery(ctx context.Context, metric metri req.Header.Set("Authorization", "Api-Token "+token) res, err := d.HttpClient.Do(req) + if err != nil { d.Log.Error(err, "Error while creating request") return "", nil, err @@ -74,9 +79,13 @@ func (d *KeptnDynatraceProvider) EvaluateQuery(ctx context.Context, metric metri err = json.Unmarshal(b, &result) if err != nil { d.Log.Error(err, "Error while parsing response") - return "", nil, err + return "", b, err + } + if !reflect.DeepEqual(result.Error, Error{}) { + err = fmt.Errorf(ErrAPIMsg, result.Error.Message) + d.Log.Error(err, "Error from Dynatrace provider") + return "", b, err } - r := fmt.Sprintf("%f", d.getSingleValue(result)) return r, b, nil } diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go index b01b1c51f84..f134a7d2ba1 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "reflect" "time" "github.com/benbjohnson/clock" @@ -36,6 +37,7 @@ type DynatraceDQLHandler struct { type DynatraceDQLResult struct { State string `json:"state"` Result DQLResult `json:"result,omitempty"` + Error `json:"error"` } type DQLResult struct { @@ -100,6 +102,7 @@ func (d *keptnDynatraceDQLProvider) EvaluateQuery(ctx context.Context, metric me d.log.Error(err, "Error while waiting for DQL query", "query", dqlHandler) return "", nil, err } + // parse result if len(results.Records) > 1 { d.log.Info("More than a single result, the first one will be used") @@ -195,5 +198,11 @@ func (d *keptnDynatraceDQLProvider) retrieveDQLResults(ctx context.Context, hand d.log.Error(err, "Error while parsing response") return result, err } + + if !reflect.DeepEqual(result.Error, Error{}) { + err = fmt.Errorf(ErrAPIMsg, result.Error.Message) + d.log.Error(err, "Error from Dynatrace DQL provider") + return nil, err + } return result, nil } diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql_test.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql_test.go index 864d4c17ac1..7ace591c613 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql_test.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql_test.go @@ -24,9 +24,12 @@ const dqlRequestHandler = `{"requestToken": "my-token"}` const dqlPayload = "{\"state\":\"SUCCEEDED\",\"result\":{\"records\":[{\"value\":{\"count\":1,\"sum\":36.50,\"min\":36.50,\"avg\":36.50,\"max\":36.50},\"metric.key\":\"dt.containers.cpu.usage_user_milli_cores\",\"timeframe\":{\"start\":\"2023-01-31T09:11:00.000Z\",\"end\":\"2023-01-31T09:12:00.`00Z\"},\"Container\":\"frontend\",\"host.name\":\"default-pool-349eb8c6-gccf\",\"k8s.namespace.name\":\"hipstershop\",\"k8s.pod.uid\":\"632df64d-474c-4410-968d-666f639ad358\"}],\"types\":[{\"mappings\":{\"value\":{\"type\":\"summary_stats\"},\"metric.key\":{\"type\":\"string\"},\"timeframe\":{\"type\":\"timeframe\"},\"Container\":{\"type\":\"string\"},\"host.name\":{\"type\":\"string\"},\"k8s.namespace.name\":{\"type\":\"string\"},\"k8s.pod.uid\":{\"type\":\"string\"}},\"indexRange\":[0,1]}]}}" const dqlPayloadNotFinished = "{\"state\":\"\",\"result\":{\"records\":[{\"value\":{\"count\":1,\"sum\":36.50,\"min\":36.78336878333334,\"avg\":36.50,\"max\":36.50},\"metric.key\":\"dt.containers.cpu.usage_user_milli_cores\",\"timeframe\":{\"start\":\"2023-01-31T09:11:00.000Z\",\"end\":\"2023-01-31T09:12:00.`00Z\"},\"Container\":\"frontend\",\"host.name\":\"default-pool-349eb8c6-gccf\",\"k8s.namespace.name\":\"hipstershop\",\"k8s.pod.uid\":\"632df64d-474c-4410-968d-666f639ad358\"}],\"types\":[{\"mappings\":{\"value\":{\"type\":\"summary_stats\"},\"metric.key\":{\"type\":\"string\"},\"timeframe\":{\"type\":\"timeframe\"},\"Container\":{\"type\":\"string\"},\"host.name\":{\"type\":\"string\"},\"k8s.namespace.name\":{\"type\":\"string\"},\"k8s.pod.uid\":{\"type\":\"string\"}},\"indexRange\":[0,1]}]}}" +const dqlPayloadError = "{\"error\":{\"code\":403,\"message\":\"Token is missing required scope\"}}" const dqlPayloadTooManyItems = "{\"state\":\"SUCCEEDED\",\"result\":{\"records\":[{\"value\":{\"count\":1,\"sum\":6.293549483333334,\"min\":6.293549483333334,\"avg\":6.293549483333334,\"max\":6.293549483333334},\"metric.key\":\"dt.containers.cpu.usage_user_milli_cores\",\"timeframe\":{\"start\":\"2023-01-31T09:07:00.000Z\",\"end\":\"2023-01-31T09:08:00.000Z\"},\"Container\":\"loginservice\",\"host.name\":\"default-pool-349eb8c6-gccf\",\"k8s.namespace.name\":\"easytrade\",\"k8s.pod.uid\":\"fc084e57-11a0-4a95-b8a0-76191c31d839\"},{\"value\":{\"count\":1,\"sum\":1.0421756,\"min\":1.0421756,\"avg\":1.0421756,\"max\":1.0421756},\"metric.key\":\"dt.containers.cpu.usage_user_milli_cores\",\"timeframe\":{\"start\":\"2023-01-31T09:07:00.000Z\",\"end\":\"2023-01-31T09:08:00.000Z\"},\"Container\":\"frontendreverseproxy\",\"host.name\":\"default-pool-349eb8c6-gccf\",\"k8s.namespace.name\":\"easytrade\",\"k8s.pod.uid\":\"41b5d6e0-98fc-4dce-a1b4-bb269a03d72b\"},{\"value\":{\"count\":1,\"sum\":6.3881383000000005,\"min\":6.3881383000000005,\"avg\":6.3881383000000005,\"max\":6.3881383000000005},\"metric.key\":\"dt.containers.cpu.usage_user_milli_cores\",\"timeframe\":{\"start\":\"2023-01-31T09:07:00.000Z\",\"end\":\"2023-01-31T09:08:00.000Z\"},\"Container\":\"shippingservice\",\"host.name\":\"default-pool-349eb8c6-gccf\",\"k8s.namespace.name\":\"hipstershop\",\"k8s.pod.uid\":\"96fcf9d7-748a-47f7-b1b3-ca6427e20edd\"}],\"types\":[{\"mappings\":{\"value\":{\"type\":\"summary_stats\"},\"metric.key\":{\"type\":\"string\"},\"timeframe\":{\"type\":\"timeframe\"},\"Container\":{\"type\":\"string\"},\"host.name\":{\"type\":\"string\"},\"k8s.namespace.name\":{\"type\":\"string\"},\"k8s.pod.uid\":{\"type\":\"string\"}},\"indexRange\":[0,3]}]}}" +var ErrUnexpected = errors.New("unexpected path") + //nolint:dupl func TestGetDQL(t *testing.T) { @@ -40,8 +43,7 @@ func TestGetDQL(t *testing.T) { if strings.Contains(path, "query:poll") { return []byte(dqlPayload), nil } - - return nil, errors.New("unexpected path") + return nil, ErrUnexpected } dqlProvider := NewKeptnDynatraceDQLProvider( @@ -82,7 +84,7 @@ func TestGetDQLMultipleRecords(t *testing.T) { return []byte(dqlPayloadTooManyItems), nil } - return nil, errors.New("unexpected path") + return nil, ErrUnexpected } dqlProvider := NewKeptnDynatraceDQLProvider( @@ -108,6 +110,46 @@ func TestGetDQLMultipleRecords(t *testing.T) { require.Contains(t, mockClient.DoCalls()[1].Path, "query:poll") } +func TestGetDQLAPIError(t *testing.T) { + + mockClient := &fake.DTAPIClientMock{} + + mockClient.DoFunc = func(ctx context.Context, path string, method string, payload []byte) ([]byte, error) { + if strings.Contains(path, "query:execute") { + return []byte(dqlRequestHandler), nil + } + + if strings.Contains(path, "query:poll") { + return []byte(dqlPayloadError), nil + } + + return nil, ErrUnexpected + } + + dqlProvider := NewKeptnDynatraceDQLProvider( + nil, + WithDTAPIClient(mockClient), + WithLogger(logr.New(klog.NewKlogr().GetSink())), + ) + + result, raw, err := dqlProvider.EvaluateQuery(context.TODO(), + metricsapi.KeptnMetric{ + Spec: metricsapi.KeptnMetricSpec{Query: ""}, + }, metricsapi.KeptnMetricsProvider{ + Spec: metricsapi.KeptnMetricsProviderSpec{}, + }, + ) + + require.NotNil(t, err) + require.Contains(t, err.Error(), "Token is missing required scope") + require.Empty(t, raw) + require.Empty(t, result) + + require.Len(t, mockClient.DoCalls(), 2) + require.Contains(t, mockClient.DoCalls()[0].Path, "query:execute") + require.Contains(t, mockClient.DoCalls()[1].Path, "query:poll") +} + func TestGetDQLTimeout(t *testing.T) { mockClient := &fake.DTAPIClientMock{} @@ -121,7 +163,7 @@ func TestGetDQLTimeout(t *testing.T) { return []byte(dqlPayloadNotFinished), nil } - return nil, errors.New("unexpected path") + return nil, ErrUnexpected } dqlProvider := NewKeptnDynatraceDQLProvider( @@ -172,7 +214,7 @@ func TestGetDQLCannotPostQuery(t *testing.T) { return nil, errors.New("oops") } - return nil, errors.New("unexpected path") + return nil, ErrUnexpected } dqlProvider := NewKeptnDynatraceDQLProvider( diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_test.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_test.go index e238498a3c1..e32bc875deb 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_test.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_test.go @@ -16,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) const dtpayload = "{\"totalCount\":1,\"nextPageKey\":null,\"resolution\":\"1m\",\"result\":[{\"metricId\":\"dsfm:billing.hostunit.assigned:splitBy():sort(value(auto,descending)):avg\",\"dataPointCountRatio\":6.0E-6,\"dimensionCountRatio\":1.0E-5,\"data\":[{\"dimensions\":[],\"dimensionMap\":{},\"timestamps\":[1666090140000,1666090200000,1666090260000,1666090320000,1666090380000,1666090440000,1666090500000,1666090560000,1666090620000,1666090680000,1666090740000,1666090800000,1666090860000,1666090920000,1666090980000,1666091040000,1666091100000,1666091160000,1666091220000,1666091280000,1666091340000,1666091400000,1666091460000,1666091520000,1666091580000,1666091640000,1666091700000,1666091760000,1666091820000,1666091880000,1666091940000,1666092000000,1666092060000,1666092120000,1666092180000,1666092240000,1666092300000,1666092360000,1666092420000,1666092480000,1666092540000,1666092600000,1666092660000,1666092720000,1666092780000,1666092840000,1666092900000,1666092960000,1666093020000,1666093080000,1666093140000,1666093200000,1666093260000,1666093320000,1666093380000,1666093440000,1666093500000,1666093560000,1666093620000,1666093680000,1666093740000,1666093800000,1666093860000,1666093920000,1666093980000,1666094040000,1666094100000,1666094160000,1666094220000,1666094280000,1666094340000,1666094400000,1666094460000,1666094520000,1666094580000,1666094640000,1666094700000,1666094760000,1666094820000,1666094880000,1666094940000,1666095000000,1666095060000,1666095120000,1666095180000,1666095240000,1666095300000,1666095360000,1666095420000,1666095480000,1666095540000,1666095600000,1666095660000,1666095720000,1666095780000,1666095840000,1666095900000,1666095960000,1666096020000,1666096080000,1666096140000,1666096200000,1666096260000,1666096320000,1666096380000,1666096440000,1666096500000,1666096560000,1666096620000,1666096680000,1666096740000,1666096800000,1666096860000,1666096920000,1666096980000,1666097040000,1666097100000,1666097160000,1666097220000,1666097280000,1666097340000],\"values\":[null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null,null,null,null,null,null,50,null,null,null,null,null,null,null,null,null]}]}]}" @@ -151,18 +152,7 @@ func TestEvaluateQuery_CorrectHTTP(t *testing.T) { require.Equal(t, 1, len(r.Header["Authorization"])) })) defer svr.Close() - fakeClient := fake.NewClient() - - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: query, - }, - } + kdp, obj := setupTest() p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ SecretKeyRef: v1.SecretKeySelector{ @@ -181,9 +171,10 @@ func TestEvaluateQuery_CorrectHTTP(t *testing.T) { } -func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { +func TestEvaluateQuery_APIError(t *testing.T) { + errorResponse := []byte("{\"error\":{\"code\":403,\"message\":\"Token is missing required scope. Use one of: metrics.read (Read metrics)\"}}") svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("garbage")) + _, err := w.Write(errorResponse) require.Nil(t, err) })) defer svr.Close() @@ -197,18 +188,44 @@ func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { secretKey: []byte(secretValue), }, } - fakeClient := fake.NewClient(apiToken) - - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, + kdp, obj := setupTest(apiToken) + p := metricsapi.KeptnMetricsProvider{ + Spec: metricsapi.KeptnMetricsProviderSpec{ + SecretKeyRef: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: secretName, + }, + Key: secretKey, + }, + TargetServer: svr.URL, + }, } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: "my-query", + r, raw, e := kdp.EvaluateQuery(context.TODO(), obj, p) + require.Equal(t, "", r) + t.Log(string(raw)) + require.Equal(t, errorResponse, raw) //we still return the raw answer to help user debug + require.NotNil(t, e) + require.Contains(t, e.Error(), "Token is missing required scope.") +} + +func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("garbage")) + require.Nil(t, err) + })) + defer svr.Close() + secretName, secretKey, secretValue := "secretName", "secretKey", "secretValue" + apiToken := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: "", + }, + Data: map[string][]byte{ + secretKey: []byte(secretValue), }, } + + kdp, obj := setupTest(apiToken) p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ SecretKeyRef: v1.SecretKeySelector{ @@ -222,7 +239,8 @@ func TestEvaluateQuery_WrongPayloadHandling(t *testing.T) { } r, raw, e := kdp.EvaluateQuery(context.TODO(), obj, p) require.Equal(t, "", r) - require.Equal(t, []byte(nil), raw) + t.Log(string(raw), e) + require.Equal(t, []byte("garbage"), raw) //we still return the raw answer to help user debug require.NotNil(t, e) } @@ -232,18 +250,8 @@ func TestEvaluateQuery_MissingSecret(t *testing.T) { require.Nil(t, err) })) defer svr.Close() - fakeClient := fake.NewClient() + kdp, obj := setupTest() - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: "my-query", - }, - } p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ TargetServer: svr.URL, @@ -260,18 +268,8 @@ func TestEvaluateQuery_SecretNotFound(t *testing.T) { require.Nil(t, err) })) defer svr.Close() - fakeClient := fake.NewClient() + kdp, obj := setupTest() - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: "my-query", - }, - } p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ SecretKeyRef: v1.SecretKeySelector{ @@ -304,18 +302,8 @@ func TestEvaluateQuery_RefNotExistingKey(t *testing.T) { secretKey: []byte(secretValue), }, } - fakeClient := fake.NewClient(apiToken) + kdp, obj := setupTest(apiToken) - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: "my-query", - }, - } missingKey := "key_not_found" p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ @@ -350,18 +338,8 @@ func TestEvaluateQuery_HappyPath(t *testing.T) { secretKey: []byte(secretValue), }, } - fakeClient := fake.NewClient(apiToken) + kdp, obj := setupTest(apiToken) - kdp := KeptnDynatraceProvider{ - HttpClient: http.Client{}, - Log: ctrl.Log.WithName("testytest"), - K8sClient: fakeClient, - } - obj := metricsapi.KeptnMetric{ - Spec: metricsapi.KeptnMetricSpec{ - Query: "my-query", - }, - } p := metricsapi.KeptnMetricsProvider{ Spec: metricsapi.KeptnMetricsProviderSpec{ SecretKeyRef: v1.SecretKeySelector{ @@ -378,3 +356,20 @@ func TestEvaluateQuery_HappyPath(t *testing.T) { require.Equal(t, []byte(dtpayload), raw) require.Equal(t, fmt.Sprintf("%f", 50.0), r) } + +func setupTest(objs ...client.Object) (KeptnDynatraceProvider, metricsapi.KeptnMetric) { + + fakeClient := fake.NewClient(objs...) + + kdp := KeptnDynatraceProvider{ + HttpClient: http.Client{}, + Log: ctrl.Log.WithName("testytest"), + K8sClient: fakeClient, + } + obj := metricsapi.KeptnMetric{ + Spec: metricsapi.KeptnMetricSpec{ + Query: "my-query", + }, + } + return kdp, obj +} diff --git a/metrics-operator/controllers/common/providers/provider_test.go b/metrics-operator/controllers/common/providers/provider_test.go index ad150fa88a1..f3869d424ab 100644 --- a/metrics-operator/controllers/common/providers/provider_test.go +++ b/metrics-operator/controllers/common/providers/provider_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/go-logr/logr" + "github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/fake" "github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/providers/datadog" "github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/providers/dynatrace" "github.com/keptn/lifecycle-toolkit/metrics-operator/controllers/common/providers/prometheus" @@ -26,6 +27,11 @@ func TestFactory(t *testing.T) { provider: &dynatrace.KeptnDynatraceProvider{}, err: false, }, + { + providerType: DynatraceDQLProviderType, + provider: dynatrace.NewKeptnDynatraceDQLProvider(fake.NewClient()), + err: false, + }, { providerType: DataDogProviderType, provider: &datadog.KeptnDataDogProvider{}, diff --git a/metrics-operator/controllers/metrics/controller.go b/metrics-operator/controllers/metrics/controller.go index 83982d16621..2d38a4c5801 100644 --- a/metrics-operator/controllers/metrics/controller.go +++ b/metrics-operator/controllers/metrics/controller.go @@ -55,10 +55,6 @@ type KeptnMetricReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the KeptnMetric object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile @@ -102,10 +98,10 @@ func (r *KeptnMetricReconciler) Reconcile(ctx context.Context, req ctrl.Request) reconcile := ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second} value, rawValue, err := provider.EvaluateQuery(ctx, *metric, *metricProvider) if err != nil { - r.Log.Error(err, "Failed to evaluate the query") + r.Log.Error(err, "Failed to evaluate the query", "Response from provider was:", (string)(rawValue)) metric.Status.ErrMsg = err.Error() metric.Status.Value = "" - metric.Status.RawValue = []byte{} + metric.Status.RawValue = cupSize(rawValue) metric.Status.LastUpdated = metav1.Time{Time: time.Now()} reconcile = ctrl.Result{Requeue: false} } else { @@ -123,6 +119,9 @@ func (r *KeptnMetricReconciler) Reconcile(ctx context.Context, req ctrl.Request) } func cupSize(value []byte) []byte { + if len(value) == 0 { + return []byte{} + } if len(value) > MB { return value[:MB] } From 76a4bd92607d05c16c63ccc4c1dd91e35cb4d6b0 Mon Sep 17 00:00:00 2001 From: RealAnna <89971034+RealAnna@users.noreply.github.com> Date: Wed, 31 May 2023 14:05:40 +0200 Subject: [PATCH 2/4] feat: add python-runtime (#1496) Signed-off-by: realanna --- python-runtime/Dockerfile | 18 ++++++++ python-runtime/README.md | 68 +++++++++++++++++++++++++++++++ python-runtime/entrypoint.sh | 12 ++++++ python-runtime/samples/args.py | 12 ++++++ python-runtime/samples/hellopy.py | 4 ++ 5 files changed, 114 insertions(+) create mode 100644 python-runtime/Dockerfile create mode 100644 python-runtime/README.md create mode 100755 python-runtime/entrypoint.sh create mode 100644 python-runtime/samples/args.py create mode 100644 python-runtime/samples/hellopy.py diff --git a/python-runtime/Dockerfile b/python-runtime/Dockerfile new file mode 100644 index 00000000000..062f7301a48 --- /dev/null +++ b/python-runtime/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9 AS production + +LABEL org.opencontainers.image.source="https://github.com/keptn/lifecycle-toolkit" \ + org.opencontainers.image.url="https://keptn.sh" \ + org.opencontainers.image.title="Keptn Python Runtime" \ + org.opencontainers.image.vendor="Keptn" \ + org.opencontainers.image.licenses="Apache-2.0" + +RUN pip install -q --disable-pip-version-check pyyaml GitPython requests + +COPY entrypoint.sh /entrypoint.sh + +USER 1000:1000 + +ENV CMD_ARGS="" +ENV SCRIPT="" + +ENTRYPOINT /entrypoint.sh diff --git a/python-runtime/README.md b/python-runtime/README.md new file mode 100644 index 00000000000..506be18caa6 --- /dev/null +++ b/python-runtime/README.md @@ -0,0 +1,68 @@ +# Keptn Lifecycle Controller - Function Runtime + +## Build + +```shell +docker build -t lifecycle-toolkit/python-runtime:${VERSION} . +``` + +## Usage + +The Keptn python runtime uses python3, and enables the follwing packages: requests, json, git, yaml + +The Keptn Lifecycle Toolkit uses this runtime to run [KeptnTask](https://lifecycle.keptn.sh/docs/tasks/write-tasks/) +for pre- and post-checks. + +`KeptnTask`s can be tested locally with the runtime using the following commands. +Replace `${VERSION}` with the KLT version of your choice. +`SCRIPT` should refer to either a python file mounted locally in the container or to a url containing the file. + +### mounting a python file + +```shell +docker run -v $(pwd)/samples/hellopy.py:/hellopy.py -e "SCRIPT=hellopy.py" -it lifecycle-toolkit/python-runtime:${VERSION} +``` + +Where the file in sample/hellopy.py contains python3 code: + +```python3 +import os + +print("Hello, World!") +print(os.environ) +``` + +This should print in your shell, something like: + +```shell +Hello, World! +environ({'HOSTNAME': 'myhost', 'PYTHON_VERSION': '3.9.16', 'PWD': '/', 'CMD_ARGS': '','SCRIPT': 'hellopy.py', ...}) +``` + +### Pass command line arguments to the python command + +You can pass python command line arguments by specifying `CMD_ARGS`. +The following example will print the help of python3: + +```shell +docker run -e "CMD_ARGS= -help" -it lifecycle-toolkit/python-runtime:${VERSION} +``` + +### Pass arguments to your python script + +In this example we pass one argument (-i test.txt) to the script + +```shell +docker run -v $(pwd)/samples/args.py:/args.py -e "SCRIPT=args.py -i test.txt" -it lifecycle-toolkit/python-runtime:${VERSION} +``` + +### Use a script from url + +We can call the hellopy.py script downloading it directly from github + +```shell +docker run -e "SCRIPT=https://raw.githubusercontent.com/keptn/lifecycle-toolkit/main/python-runtime/samples/hellopy.py" -it lifecycle-toolkit/python-runtime:${VERSION} +``` + + + diff --git a/python-runtime/entrypoint.sh b/python-runtime/entrypoint.sh new file mode 100755 index 00000000000..d1628f229ee --- /dev/null +++ b/python-runtime/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -eu + +regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]' + +if [[ $SCRIPT =~ $regex ]] +then + curl -s $SCRIPT | python3 $CMD_ARGS - +else + python3 $CMD_ARGS $SCRIPT +fi diff --git a/python-runtime/samples/args.py b/python-runtime/samples/args.py new file mode 100644 index 00000000000..0dc3aba098b --- /dev/null +++ b/python-runtime/samples/args.py @@ -0,0 +1,12 @@ +import sys, getopt + +def main(argv): + inputfile = '' + opts, _ = getopt.getopt(argv,"i:",["ifile="]) + for opt, arg in opts: + if opt in ("-i", "--ifile"): + inputfile = arg + print ('Input file is ', inputfile) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python-runtime/samples/hellopy.py b/python-runtime/samples/hellopy.py new file mode 100644 index 00000000000..0d973a7557c --- /dev/null +++ b/python-runtime/samples/hellopy.py @@ -0,0 +1,4 @@ +import os + +print("Hello, World!") +print(os.environ) From 0f28b8c2b5854944d9b0e72fee97a6bc91f39bea Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 31 May 2023 14:30:06 +0200 Subject: [PATCH 3/4] fix(cert-manager): avoid index-out-of-bounds error when updating webhook configs (#1497) Signed-off-by: Florian Bacher --- .../keptnwebhookcertificate_controller.go | 2 +- .../keptnwebhookcontroller/webhook_cert_controller_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/klt-cert-manager/controllers/keptnwebhookcontroller/keptnwebhookcertificate_controller.go b/klt-cert-manager/controllers/keptnwebhookcontroller/keptnwebhookcertificate_controller.go index a085394d882..c1414121f4c 100644 --- a/klt-cert-manager/controllers/keptnwebhookcontroller/keptnwebhookcertificate_controller.go +++ b/klt-cert-manager/controllers/keptnwebhookcontroller/keptnwebhookcertificate_controller.go @@ -180,7 +180,7 @@ func (r *KeptnWebhookCertificateReconciler) updateConfigurations(ctx context.Con } for i := range validatingWebhookConfigurationList.Items { - r.Log.Info("injecting certificate into validating webhook config", "vwc", mutatingWebhookConfigurationList.Items[i].Name) + r.Log.Info("injecting certificate into validating webhook config", "vwc", validatingWebhookConfigurationList.Items[i].Name) if err := r.updateClientConfigurations(ctx, bundle, validatingWebhookConfigs, &validatingWebhookConfigurationList.Items[i]); err != nil { return err } diff --git a/klt-cert-manager/controllers/keptnwebhookcontroller/webhook_cert_controller_test.go b/klt-cert-manager/controllers/keptnwebhookcontroller/webhook_cert_controller_test.go index bcec3a5e66b..3e148b11db2 100644 --- a/klt-cert-manager/controllers/keptnwebhookcontroller/webhook_cert_controller_test.go +++ b/klt-cert-manager/controllers/keptnwebhookcontroller/webhook_cert_controller_test.go @@ -146,7 +146,8 @@ func TestReconcile(t *testing.T) { t.Run(`reconcile successfully with mutatingwebhookconfiguration`, func(t *testing.T) { fakeClient := fake.NewClient(crd1, crd2, crd3, &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: "my-mutating-webhook-config", + Name: "my-mutating-webhook-config", + Labels: getMatchLabel(), }, Webhooks: []admissionregistrationv1.MutatingWebhook{ { @@ -167,7 +168,8 @@ func TestReconcile(t *testing.T) { t.Run(`reconcile successfully with validatingwebhookconfiguration`, func(t *testing.T) { fakeClient := fake.NewClient(crd1, crd2, crd3, &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: "my-validating-webhook-config", + Name: "my-validating-webhook-config", + Labels: getMatchLabel(), }, Webhooks: []admissionregistrationv1.ValidatingWebhook{ { From 02ce86023b3db175481b859f379cb4298d03566a Mon Sep 17 00:00:00 2001 From: odubajDT <93584209+odubajDT@users.noreply.github.com> Date: Thu, 1 Jun 2023 10:05:16 +0200 Subject: [PATCH 4/4] feat(operator): introduce container-runtime runner (#1493) Signed-off-by: odubajDT Signed-off-by: odubajDT <93584209+odubajDT@users.noreply.github.com> Co-authored-by: Giovanni Liva --- examples/sample-app/base/container-task.yaml | 12 + examples/sample-app/version-1/app.yaml | 2 + examples/sample-app/version-2/app.yaml | 2 + examples/sample-app/version-3/app.yaml | 2 + .../v1alpha3/keptntaskdefinition_types.go | 26 +- .../keptntaskdefinition_types_test.go | 167 +++ .../v1alpha3/zz_generated.deepcopy.go | 17 +- ...fecycle.keptn.sh_keptntaskdefinitions.yaml | 1231 +++++++++++++++++ ...ifecycle_v1alpha3_keptntaskdefinition.yaml | 6 - ...1alpha3_keptntaskdefinition_container.yaml | 19 + ...cycle_v1alpha3_keptntaskdefinition_js.yaml | 10 + .../lifecycle/keptntask/container_builder.go | 65 +- .../keptntask/container_builder_test.go | 244 ++++ .../lifecycle/keptntask/job_runner_builder.go | 37 + .../keptntask/job_runner_builder_test.go | 62 + .../lifecycle/keptntask/job_utils.go | 7 +- .../lifecycle/keptntask/job_utils_test.go | 2 +- .../lifecycle/keptntask/js_builder.go | 4 +- .../lifecycle/keptntask/js_builder_test.go | 8 +- .../keptntaskdefinition/reconcile_function.go | 3 + operator/test/component/task/task_test.go | 2 +- .../taskdefinition/taskdefinition_test.go | 6 +- .../container-runtime/00-assert.yaml | 64 + .../container-runtime/00-install.yaml | 40 + .../container-runtime/00-teststep.yaml | 4 + 25 files changed, 1990 insertions(+), 52 deletions(-) create mode 100644 examples/sample-app/base/container-task.yaml create mode 100644 operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go delete mode 100644 operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition.yaml create mode 100644 operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_container.yaml create mode 100644 operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_js.yaml create mode 100644 operator/controllers/lifecycle/keptntask/container_builder_test.go create mode 100644 operator/controllers/lifecycle/keptntask/job_runner_builder.go create mode 100644 operator/controllers/lifecycle/keptntask/job_runner_builder_test.go create mode 100644 test/integration/container-runtime/00-assert.yaml create mode 100644 test/integration/container-runtime/00-install.yaml create mode 100644 test/integration/container-runtime/00-teststep.yaml diff --git a/examples/sample-app/base/container-task.yaml b/examples/sample-app/base/container-task.yaml new file mode 100644 index 00000000000..b6001490dad --- /dev/null +++ b/examples/sample-app/base/container-task.yaml @@ -0,0 +1,12 @@ +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTaskDefinition +metadata: + name: container-sleep +spec: + container: + name: testy-test + image: busybox:1.36.0 + command: + - 'sh' + - '-c' + - 'sleep 30' diff --git a/examples/sample-app/version-1/app.yaml b/examples/sample-app/version-1/app.yaml index 7358f7fbe4a..ded6ef9192b 100644 --- a/examples/sample-app/version-1/app.yaml +++ b/examples/sample-app/version-1/app.yaml @@ -16,3 +16,5 @@ spec: version: 0.1.1 preDeploymentEvaluations: - app-pre-deploy-eval-1 + preDeploymentTasks: + - container-sleep diff --git a/examples/sample-app/version-2/app.yaml b/examples/sample-app/version-2/app.yaml index d43ed4a0047..1801433737d 100644 --- a/examples/sample-app/version-2/app.yaml +++ b/examples/sample-app/version-2/app.yaml @@ -16,3 +16,5 @@ spec: version: 0.1.1 preDeploymentEvaluations: - app-pre-deploy-eval-2 + preDeploymentTasks: + - container-sleep diff --git a/examples/sample-app/version-3/app.yaml b/examples/sample-app/version-3/app.yaml index 3c33d0f0ca7..3bb50a1dc59 100644 --- a/examples/sample-app/version-3/app.yaml +++ b/examples/sample-app/version-3/app.yaml @@ -14,3 +14,5 @@ spec: version: 0.1.1 - name: podtato-head-hat version: 0.1.2 + preDeploymentTasks: + - container-sleep diff --git a/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go b/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go index 95df2aef369..93b45fe139d 100644 --- a/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go +++ b/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha3 import ( + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,7 +28,12 @@ import ( type KeptnTaskDefinitionSpec struct { // Function contains the definition for the function that is to be executed in KeptnTasks based on // the KeptnTaskDefinitions. - Function FunctionSpec `json:"function,omitempty"` + // +optional + Function *FunctionSpec `json:"function,omitempty"` + // Container contains the definition for the container that is to be used in Job based on + // the KeptnTaskDefinitions. + // +optional + Container *ContainerSpec `json:"container,omitempty"` // Retries specifies how many times a job executing the KeptnTaskDefinition should be restarted in the case // of an unsuccessful attempt. // +kubebuilder:default:=10 @@ -39,7 +45,6 @@ type KeptnTaskDefinitionSpec struct { // +kubebuilder:default:="5m" // +kubebuilder:validation:Pattern="^0|([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" // +kubebuilder:validation:Type:=string - // +optional Timeout metav1.Duration `json:"timeout,omitempty"` } @@ -85,6 +90,7 @@ type HttpReference struct { } type ContainerSpec struct { + *v1.Container `json:",inline"` } // KeptnTaskDefinitionStatus defines the observed state of KeptnTaskDefinition @@ -125,3 +131,19 @@ type KeptnTaskDefinitionList struct { func init() { SchemeBuilder.Register(&KeptnTaskDefinition{}, &KeptnTaskDefinitionList{}) } + +func (d KeptnTaskDefinition) SpecExists() bool { + return d.IsJSSpecDefined() || d.IsContainerSpecDefined() +} + +func (d KeptnTaskDefinition) IsJSSpecDefined() bool { + return d.Spec.Function != nil +} + +func (d KeptnTaskDefinition) IsContainerSpecDefined() bool { + return d.Spec.Container != nil +} + +func (d KeptnTaskDefinition) IsVolumeMountPresent() bool { + return d.IsContainerSpecDefined() && d.Spec.Container.VolumeMounts != nil && len(d.Spec.Container.VolumeMounts) > 0 +} diff --git a/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go b/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go new file mode 100644 index 00000000000..3f141d7c2f3 --- /dev/null +++ b/operator/apis/lifecycle/v1alpha3/keptntaskdefinition_types_test.go @@ -0,0 +1,167 @@ +package v1alpha3 + +import ( + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" +) + +var jsTaskDef = &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + Function: &FunctionSpec{ + Inline: Inline{ + Code: "some code", + }, + }, + }, +} + +var containerTaskDef = &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + Container: &ContainerSpec{ + Container: &v1.Container{ + Image: "image", + }, + }, + }, +} + +func Test_SpecExists(t *testing.T) { + tests := []struct { + name string + taskDef *KeptnTaskDefinition + want bool + }{ + { + name: "js builder", + taskDef: jsTaskDef, + want: true, + }, + { + name: "container builder", + taskDef: containerTaskDef, + want: true, + }, + { + name: "empty builder", + taskDef: &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.taskDef.SpecExists()) + }) + } +} + +func Test_IsJSSpecDefined(t *testing.T) { + tests := []struct { + name string + taskDef *KeptnTaskDefinition + want bool + }{ + { + name: "defined", + taskDef: jsTaskDef, + want: true, + }, + { + name: "empty", + taskDef: &KeptnTaskDefinition{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.taskDef.IsJSSpecDefined()) + }) + } +} + +func Test_IsVolumeMountPresent(t *testing.T) { + tests := []struct { + name string + taskDef *KeptnTaskDefinition + want bool + }{ + { + name: "defined", + taskDef: &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + Container: &ContainerSpec{ + Container: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{ + { + Name: "name", + MountPath: "path", + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "empty", + taskDef: &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + Container: &ContainerSpec{ + Container: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{}, + }, + }, + }, + }, + want: false, + }, + { + name: "nil", + taskDef: &KeptnTaskDefinition{ + Spec: KeptnTaskDefinitionSpec{ + Container: &ContainerSpec{ + Container: &v1.Container{ + Image: "image", + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.taskDef.IsVolumeMountPresent()) + }) + } +} + +func Test_IsContainerSpecDefined(t *testing.T) { + tests := []struct { + name string + taskDef *KeptnTaskDefinition + want bool + }{ + { + name: "defined", + taskDef: containerTaskDef, + want: true, + }, + { + name: "empty", + taskDef: &KeptnTaskDefinition{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.taskDef.IsContainerSpecDefined()) + }) + } +} diff --git a/operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go b/operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go index 7ee7cd41e2c..cc68f44e91e 100644 --- a/operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go +++ b/operator/apis/lifecycle/v1alpha3/zz_generated.deepcopy.go @@ -24,6 +24,7 @@ package v1alpha3 import ( "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3/common" "go.opentelemetry.io/otel/propagation" + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -45,6 +46,11 @@ func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { *out = *in + if in.Container != nil { + in, out := &in.Container, &out.Container + *out = new(v1.Container) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerSpec. @@ -909,7 +915,16 @@ func (in *KeptnTaskDefinitionList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeptnTaskDefinitionSpec) DeepCopyInto(out *KeptnTaskDefinitionSpec) { *out = *in - in.Function.DeepCopyInto(&out.Function) + if in.Function != nil { + in, out := &in.Function, &out.Function + *out = new(FunctionSpec) + (*in).DeepCopyInto(*out) + } + if in.Container != nil { + in, out := &in.Container, &out.Container + *out = new(ContainerSpec) + (*in).DeepCopyInto(*out) + } if in.Retries != nil { in, out := &in.Retries, &out.Retries *out = new(int32) diff --git a/operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml b/operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml index f6be0e8b34c..d88447aeefb 100644 --- a/operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml +++ b/operator/config/crd/bases/lifecycle.keptn.sh_keptntaskdefinitions.yaml @@ -189,6 +189,1237 @@ spec: spec: description: Spec describes the desired state of the KeptnTaskDefinition. properties: + container: + description: Container contains the definition for the container that + is to be used in Job based on the KeptnTaskDefinitions. + properties: + args: + description: 'Arguments to the entrypoint. The container image''s + CMD is used if this is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. If a variable + cannot be resolved, the reference in the input string will be + unchanged. Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + produce the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within a shell. The + container image''s ENTRYPOINT is used if this is not provided. + Variable references $(VAR_NAME) are expanded using the container''s + environment. If a variable cannot be resolved, the reference + in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: + i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether + the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in + the container and any service environment variables. If + a variable cannot be resolved, the reference in the input + string will be unchanged. Double $$ are reduced to a single + $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, + `metadata.annotations['''']`, spec.nodeName, + spec.serviceAccountName, status.hostIP, status.podIP, + status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment variables + in the container. The keys defined within a source must be a + C_IDENTIFIER. All invalid keys will be reported as an event + when the container is starting. When a key exists in multiple + sources, the value associated with the last source will take + precedence. Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of a set of + ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each key + in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management + to default or override container images in workload controllers + like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent + otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system should take in + response to container lifecycle events. Cannot be updated. + properties: + postStart: + description: 'PostStart is called immediately after a container + is created. If the handler fails, the container is terminated + and restarted according to its restart policy. Other management + of the container blocks until the hook completes. More info: + https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to execute + inside the container, the working directory for + the command is root ('/') in the container's filesystem. + The command is simply exec'd, it is not run inside + a shell, so traditional shell instructions ('|', + etc) won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is treated + as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: Host name to connect to, defaults to + the pod IP. You probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: The header field name. This will + be canonicalized upon output, so case-variant + names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range 1 + to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported as + a LifecycleHandler and kept for the backward compatibility. + There are no validation of this field and lifecycle + hooks will fail in runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range 1 + to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before a container + is terminated due to an API request or management event + such as liveness/startup probe failure, preemption, resource + contention, etc. The handler is not called if the container + crashes or exits. The Pod''s termination grace period countdown + begins before the PreStop hook is executed. Regardless of + the outcome of the handler, the container will eventually + terminate within the Pod''s termination grace period (unless + delayed by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to execute + inside the container, the working directory for + the command is root ('/') in the container's filesystem. + The command is simply exec'd, it is not run inside + a shell, so traditional shell instructions ('|', + etc) won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is treated + as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: Host name to connect to, defaults to + the pod IP. You probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: The header field name. This will + be canonicalized upon output, so case-variant + names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range 1 + to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported as + a LifecycleHandler and kept for the backward compatibility. + There are no validation of this field and lifecycle + hooks will fail in runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range 1 + to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container liveness. Container + will be restarted if the probe fails. Cannot be updated. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to execute inside + the container, the working directory for the command is + root ('/') in the container's filesystem. The command + is simply exec'd, it is not run inside a shell, so traditional + shell instructions ('|', etc) won't work. To use a shell, + you need to explicitly call out to that shell. Exit + status of 0 is treated as live/healthy and non-zero + is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + This is a beta field and requires enabling GRPCContainerProbe + feature gate. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service to place + in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior is + defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: Host name to connect to, defaults to the + pod IP. You probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: The header field name. This will be + canonicalized upon output, so case-variant names + will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container has started + before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod needs to + terminate gracefully upon probe failure. The grace period + is the duration in seconds after the processes running in + the pod are sent a termination signal and the time when + the processes are forcibly halted with a kill signal. Set + this value longer than the expected cleanup time for your + process. If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides the value + provided by the pod spec. Value must be non-negative integer. + The value zero indicates stop immediately via the kill signal + (no opportunity to shut down). This is a beta field and + requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is + used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. More info: + https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified as a DNS_LABEL. Each + container in a pod must have a unique name (DNS_LABEL). Cannot + be updated. + type: string + ports: + description: List of ports to expose from the container. Not specifying + a port here DOES NOT prevent that port from being exposed. Any + port which is listening on the default "0.0.0.0" address inside + a container will be accessible from the network. Modifying this + array with strategic merge patch may corrupt the data. For more + information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a single + container. + properties: + containerPort: + description: Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: Number of port to expose on the host. If specified, + this must be a valid port number, 0 < x < 65536. If HostNetwork + is specified, this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in a pod must + have a unique name. Name for the port that can be referred + to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container service readiness. Container + will be removed from service endpoints if the probe fails. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to execute inside + the container, the working directory for the command is + root ('/') in the container's filesystem. The command + is simply exec'd, it is not run inside a shell, so traditional + shell instructions ('|', etc) won't work. To use a shell, + you need to explicitly call out to that shell. Exit + status of 0 is treated as live/healthy and non-zero + is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + This is a beta field and requires enabling GRPCContainerProbe + feature gate. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service to place + in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior is + defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: Host name to connect to, defaults to the + pod IP. You probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: The header field name. This will be + canonicalized upon output, so case-variant names + will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container has started + before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod needs to + terminate gracefully upon probe failure. The grace period + is the duration in seconds after the processes running in + the pod are sent a termination signal and the time when + the processes are forcibly halted with a kill signal. Set + this value longer than the expected cleanup time for your + process. If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides the value + provided by the pod spec. Value must be non-negative integer. + The value zero indicates stop immediately via the kill signal + (no opportunity to shut down). This is a beta field and + requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is + used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. More info: + https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by this container. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + claims: + description: "Claims lists the names of resources, defined + in spec.resourceClaims, that are used by this container. + \n This is an alpha field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable. It can only be + set for containers." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec.resourceClaims of the Pod where this field + is used. It makes that resource available inside a + container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the security options the + container should be run with. If set, the fields of SecurityContext + override the equivalent fields of PodSecurityContext. More info: + https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls whether a + process can gain more privileges than its parent process. + This bool directly controls if the no_new_privs flag will + be set on the container process. AllowPrivilegeEscalation + is true always when the container is: 1) run as Privileged + 2) has CAP_SYS_ADMIN Note that this field cannot be set + when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the + container runtime. Note that this field cannot be set when + spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes in + privileged containers are essentially equivalent to root + on the host. Defaults to false. Note that this field cannot + be set when spec.os.name is windows. + type: boolean + procMount: + description: procMount denotes the type of proc mount to use + for the containers. The default is DefaultProcMount which + uses the container runtime defaults for readonly paths and + masked paths. This requires the ProcMountType feature flag + to be enabled. Note that this field cannot be set when spec.os.name + is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only root filesystem. + Default is false. Note that this field cannot be set when + spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the container + process. Uses runtime default if unset. May also be set + in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext + takes precedence. Note that this field cannot be set when + spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. If true, the Kubelet will validate the image at runtime + to ensure that it does not run as UID 0 (root) and fail + to start the container if it does. If unset or false, no + such validation will be performed. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, the + value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the container + process. Defaults to user specified in image metadata if + unspecified. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, the + value specified in SecurityContext takes precedence. Note + that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random + SELinux context for each container. May also be set in + PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext + takes precedence. Note that this field cannot be set when + spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this container. + If seccomp options are provided at both the pod & container + level, the container options override the pod options. Note + that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. The profile must + be preconfigured on the node to work. Must be a descending + path, relative to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp profile + will be applied. Valid options are: \n Localhost - a + profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile + should be used. Unconfined - no profile should be applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied to all + containers. If unspecified, the options from the PodSecurityContext + will be used. If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is + linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the GMSA admission + webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential spec named + by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container should + be run as a 'Host Process' container. This field is + alpha-level and will only be honored by components that + enable the WindowsHostProcessContainers feature flag. + Setting this field without the feature flag will result + in errors when validating the Pod. All of a Pod's containers + must have the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, if HostProcess + is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. Defaults to the user specified + in image metadata if unspecified. May also be set in + PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext + takes precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that the Pod has successfully + initialized. If specified, no other probes are executed until + this completes successfully. If this probe fails, the Pod will + be restarted, just as if the livenessProbe failed. This can + be used to provide different probe parameters at the beginning + of a Pod''s lifecycle, when it might take a long time to load + data or warm a cache, than during steady-state operation. This + cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to execute inside + the container, the working directory for the command is + root ('/') in the container's filesystem. The command + is simply exec'd, it is not run inside a shell, so traditional + shell instructions ('|', etc) won't work. To use a shell, + you need to explicitly call out to that shell. Exit + status of 0 is treated as live/healthy and non-zero + is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the probe to + be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + This is a beta field and requires enabling GRPCContainerProbe + feature gate. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service to place + in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior is + defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: Host name to connect to, defaults to the + pod IP. You probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: The header field name. This will be + canonicalized upon output, so case-variant names + will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container has started + before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to + 1. Must be 1 for liveness and startup. Minimum value is + 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access on the + container. Number must be in the range 1 to 65535. Name + must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod needs to + terminate gracefully upon probe failure. The grace period + is the duration in seconds after the processes running in + the pod are sent a termination signal and the time when + the processes are forcibly halted with a kill signal. Set + this value longer than the expected cleanup time for your + process. If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides the value + provided by the pod spec. Value must be non-negative integer. + The value zero indicates stop immediately via the kill signal + (no opportunity to shut down). This is a beta field and + requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is + used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the probe times + out. Defaults to 1 second. Minimum value is 1. More info: + https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate a buffer for + stdin in the container runtime. If this is not set, reads from + stdin in the container will always result in EOF. Default is + false. + type: boolean + stdinOnce: + description: Whether the container runtime should close the stdin + channel after it has been opened by a single attach. When stdin + is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container + start, is empty until the first client attaches to stdin, and + then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container + is restarted. If this flag is false, a container processes that + reads from stdin will never receive an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the file to which the container''s + termination message will be written is mounted into the container''s + filesystem. Message written is intended to be brief final status, + such as an assertion failure message. Will be truncated by the + node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. Defaults to /dev/termination-log. + Cannot be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should be populated. + File will use the contents of terminationMessagePath to populate + the container status message on both success and failure. FallbackToLogsOnError + will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever + is smaller. Defaults to File. Cannot be updated. + type: string + tty: + description: Whether this container should allocate a TTY for + itself, also requires 'stdin' to be true. Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume within + a container. + properties: + mountPath: + description: Path within the container at which the volume + should be mounted. Must not contain ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts are + propagated from the host to container and the other way + around. When not set, MountPropagationNone is used. This + field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write otherwise + (false or unspecified). Defaults to false. + type: boolean + subPath: + description: Path within the volume from which the container's + volume should be mounted. Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which + the container's volume should be mounted. Behaves similarly + to SubPath but environment variable references $(VAR_NAME) + are expanded using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath are mutually + exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which might be + configured in the container image. Cannot be updated. + type: string + required: + - name + type: object function: description: Function contains the definition for the function that is to be executed in KeptnTasks based on the KeptnTaskDefinitions. diff --git a/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition.yaml b/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition.yaml deleted file mode 100644 index 5d10c28faee..00000000000 --- a/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: lifecycle.keptn.sh/v1alpha3 -kind: KeptnTaskDefinition -metadata: - name: keptntaskdefinition-sample -spec: -# TODO(user): Add fields here diff --git a/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_container.yaml b/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_container.yaml new file mode 100644 index 00000000000..a0cbfcffc0c --- /dev/null +++ b/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_container.yaml @@ -0,0 +1,19 @@ +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTaskDefinition +metadata: + name: keptntaskdefinition-sample-container +spec: + container: + name: testy-test + image: busybox:1.36.0 + resources: + limits: + memory: "200Mi" + command: + - 'echo' + - 'Hello World!' + - '>' + - '/cache/log.txt' + volumeMounts: + - mountPath: /cache + name: logger diff --git a/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_js.yaml b/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_js.yaml new file mode 100644 index 00000000000..83a33dfc3c4 --- /dev/null +++ b/operator/config/samples/lifecycle_v1alpha3_keptntaskdefinition_js.yaml @@ -0,0 +1,10 @@ +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTaskDefinition +metadata: + name: keptntaskdefinition-sample-js +spec: + function: + inline: + code: | + console.log('hello'); + retries: 2 diff --git a/operator/controllers/lifecycle/keptntask/container_builder.go b/operator/controllers/lifecycle/keptntask/container_builder.go index e550b9dfac3..098db9bb611 100644 --- a/operator/controllers/lifecycle/keptntask/container_builder.go +++ b/operator/controllers/lifecycle/keptntask/container_builder.go @@ -1,46 +1,53 @@ package keptntask import ( - "reflect" - - "github.com/go-logr/logr" klcv1alpha3 "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" "golang.org/x/net/context" corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/api/resource" ) -// IContainerBuilder is the interface that describes the operations needed to help build job specs of a task -type IContainerBuilder interface { - // CreateContainerWithVolumes returns a job container and volumes based on the task definition spec - CreateContainerWithVolumes(ctx context.Context) (*corev1.Container, []corev1.Volume, error) +// ContainerBuilder implements container builder interface for python +type ContainerBuilder struct { + taskDef *klcv1alpha3.KeptnTaskDefinition } -// BuilderOptions contains everything needed to build the current job -type BuilderOptions struct { - client.Client - recorder record.EventRecorder - req ctrl.Request - Log logr.Logger - task *klcv1alpha3.KeptnTask - taskDef *klcv1alpha3.KeptnTaskDefinition +func NewContainerBuilder(taskDef *klcv1alpha3.KeptnTaskDefinition) *ContainerBuilder { + return &ContainerBuilder{ + taskDef: taskDef, + } } -func getContainerBuilder(options BuilderOptions) IContainerBuilder { - if isJSSpecDefined(&options.taskDef.Spec) { - builder := newJSBuilder(options) - return &builder - } - return nil +func (c *ContainerBuilder) CreateContainerWithVolumes(ctx context.Context) (*corev1.Container, []corev1.Volume, error) { + return c.taskDef.Spec.Container.Container, c.generateVolumes(), nil } -func specExists(definition *klcv1alpha3.KeptnTaskDefinition) bool { - //TODO when adding new builders add more logic here - return isJSSpecDefined(&definition.Spec) +func (c *ContainerBuilder) getVolumeSource() *corev1.EmptyDirVolumeSource { + quantity, ok := c.taskDef.Spec.Container.Resources.Limits["memory"] + if ok { + return &corev1.EmptyDirVolumeSource{ + SizeLimit: &quantity, + Medium: corev1.StorageMedium("Memory"), + } + } + + return &corev1.EmptyDirVolumeSource{ + // Default 50% of the memory of the node, max 1Gi + SizeLimit: resource.NewQuantity(1, resource.Format("Gi")), + Medium: corev1.StorageMedium("Memory"), + } } -func isJSSpecDefined(spec *klcv1alpha3.KeptnTaskDefinitionSpec) bool { - return !reflect.DeepEqual(spec.Function, klcv1alpha3.FunctionSpec{}) +func (c *ContainerBuilder) generateVolumes() []corev1.Volume { + if !c.taskDef.IsVolumeMountPresent() { + return []corev1.Volume{} + } + return []corev1.Volume{ + { + Name: c.taskDef.Spec.Container.VolumeMounts[0].Name, + VolumeSource: corev1.VolumeSource{ + EmptyDir: c.getVolumeSource(), + }, + }, + } } diff --git a/operator/controllers/lifecycle/keptntask/container_builder_test.go b/operator/controllers/lifecycle/keptntask/container_builder_test.go new file mode 100644 index 00000000000..bcdaee78c17 --- /dev/null +++ b/operator/controllers/lifecycle/keptntask/container_builder_test.go @@ -0,0 +1,244 @@ +package keptntask + +import ( + "context" + "testing" + + "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestContainerBuilder_CreateContainerWithVolumes(t *testing.T) { + tests := []struct { + name string + builder ContainerBuilder + wantContainer *v1.Container + wantVolumes []v1.Volume + }{ + { + name: "defined without volumes", + builder: ContainerBuilder{ + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + }, + }, + }, + }, + }, + wantContainer: &v1.Container{ + Image: "image", + }, + wantVolumes: []v1.Volume{}, + }, + { + name: "defined with volume", + builder: ContainerBuilder{ + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{ + { + Name: "test-volume", + MountPath: "path", + }, + }, + }, + }, + }, + }, + }, + wantContainer: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{ + { + Name: "test-volume", + MountPath: "path", + }, + }, + }, + wantVolumes: []v1.Volume{ + { + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(1, resource.Format("Gi")), + Medium: v1.StorageMedium("Memory"), + }, + }, + }, + }, + }, + { + name: "defined with volume and limits", + builder: ContainerBuilder{ + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "memory": *resource.NewQuantity(100, resource.Format("Mi")), + }, + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "test-volume", + MountPath: "path", + }, + }, + }, + }, + }, + }, + }, + wantContainer: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{ + { + Name: "test-volume", + MountPath: "path", + }, + }, + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "memory": *resource.NewQuantity(100, resource.Format("Mi")), + }, + }, + }, + wantVolumes: []v1.Volume{ + { + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(100, resource.Format("Mi")), + Medium: v1.StorageMedium("Memory"), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + container, volumes, _ := tt.builder.CreateContainerWithVolumes(context.TODO()) + require.Equal(t, tt.wantContainer, container) + require.Equal(t, tt.wantVolumes, volumes) + }) + } +} + +func Test_GenerateVolumes(t *testing.T) { + tests := []struct { + name string + taskDef *v1alpha3.KeptnTaskDefinition + want []v1.Volume + }{ + { + name: "defined", + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + VolumeMounts: []v1.VolumeMount{ + { + Name: "name", + MountPath: "path", + }, + }, + }, + }, + }, + }, + want: []v1.Volume{ + { + Name: "name", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(1, resource.Format("Gi")), + Medium: v1.StorageMedium("Memory"), + }, + }, + }, + }, + }, + { + name: "empty", + taskDef: &v1alpha3.KeptnTaskDefinition{}, + want: []v1.Volume{}, + }, + } + for _, tt := range tests { + builder := ContainerBuilder{ + taskDef: tt.taskDef, + } + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, builder.generateVolumes()) + }) + } +} + +func Test_GetVolumeSource(t *testing.T) { + tests := []struct { + name string + taskDef *v1alpha3.KeptnTaskDefinition + want *v1.EmptyDirVolumeSource + }{ + { + name: "not set limits", + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{}, + }, + }, + }, + }, + }, + want: &v1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(1, resource.Format("Gi")), + Medium: v1.StorageMedium("Memory"), + }, + }, + { + name: "set limits", + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "memory": *resource.NewQuantity(100, resource.Format("Mi")), + }, + }, + }, + }, + }, + }, + want: &v1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(100, resource.Format("Mi")), + Medium: v1.StorageMedium("Memory"), + }, + }, + } + for _, tt := range tests { + builder := ContainerBuilder{ + taskDef: tt.taskDef, + } + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, builder.getVolumeSource()) + }) + } +} diff --git a/operator/controllers/lifecycle/keptntask/job_runner_builder.go b/operator/controllers/lifecycle/keptntask/job_runner_builder.go new file mode 100644 index 00000000000..d177c0cae65 --- /dev/null +++ b/operator/controllers/lifecycle/keptntask/job_runner_builder.go @@ -0,0 +1,37 @@ +package keptntask + +import ( + "github.com/go-logr/logr" + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" + "golang.org/x/net/context" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// JobRunnerBuilder is the interface that describes the operations needed to help build job specs of a task +type JobRunnerBuilder interface { + // CreateContainerWithVolumes returns a job container and volumes based on the task definition spec + CreateContainerWithVolumes(ctx context.Context) (*corev1.Container, []corev1.Volume, error) +} + +// BuilderOptions contains everything needed to build the current job +type BuilderOptions struct { + client.Client + recorder record.EventRecorder + req ctrl.Request + Log logr.Logger + task *klcv1alpha3.KeptnTask + taskDef *klcv1alpha3.KeptnTaskDefinition +} + +func getJobRunnerBuilder(options BuilderOptions) JobRunnerBuilder { + if options.taskDef.IsJSSpecDefined() { + return NewJSBuilder(options) + } + if options.taskDef.IsContainerSpecDefined() { + return NewContainerBuilder(options.taskDef) + } + return nil +} diff --git a/operator/controllers/lifecycle/keptntask/job_runner_builder_test.go b/operator/controllers/lifecycle/keptntask/job_runner_builder_test.go new file mode 100644 index 00000000000..36f23442e1c --- /dev/null +++ b/operator/controllers/lifecycle/keptntask/job_runner_builder_test.go @@ -0,0 +1,62 @@ +package keptntask + +import ( + "testing" + + "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" +) + +func Test_getJobRunnerBuilder(t *testing.T) { + jsBuilderOptions := BuilderOptions{ + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Function: &v1alpha3.FunctionSpec{ + Inline: v1alpha3.Inline{ + Code: "some code", + }, + }, + }, + }, + } + containerBuilderOptions := BuilderOptions{ + taskDef: &v1alpha3.KeptnTaskDefinition{ + Spec: v1alpha3.KeptnTaskDefinitionSpec{ + Container: &v1alpha3.ContainerSpec{ + Container: &v1.Container{ + Image: "image", + }, + }, + }, + }, + } + tests := []struct { + name string + options BuilderOptions + want JobRunnerBuilder + }{ + { + name: "js builder", + options: jsBuilderOptions, + want: NewJSBuilder(jsBuilderOptions), + }, + { + name: "container builder", + options: containerBuilderOptions, + want: NewContainerBuilder(containerBuilderOptions.taskDef), + }, + { + name: "invalid builder", + options: BuilderOptions{ + taskDef: &v1alpha3.KeptnTaskDefinition{}, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, getJobRunnerBuilder(tt.options)) + }) + } +} diff --git a/operator/controllers/lifecycle/keptntask/job_utils.go b/operator/controllers/lifecycle/keptntask/job_utils.go index 61eadf52381..8d58bb96dd2 100644 --- a/operator/controllers/lifecycle/keptntask/job_utils.go +++ b/operator/controllers/lifecycle/keptntask/job_utils.go @@ -24,7 +24,7 @@ func (r *KeptnTaskReconciler) createJob(ctx context.Context, req ctrl.Request, t return err } - if specExists(definition) { + if definition.SpecExists() { jobName, err = r.createFunctionJob(ctx, req, task, definition) if err != nil { return err @@ -141,12 +141,13 @@ func (r *KeptnTaskReconciler) generateJob(ctx context.Context, task *klcv1alpha3 taskDef: definition, recorder: r.Recorder, } - builder := getContainerBuilder(builderOpt) + + builder := getJobRunnerBuilder(builderOpt) if builder == nil { return nil, controllererrors.ErrNoTaskDefinitionSpec } - container, volumes, err := builder.CreateContainerWithVolumes(ctx) + container, volumes, err := builder.CreateContainerWithVolumes(ctx) if err != nil { return nil, controllererrors.ErrCannotMarshalParams } diff --git a/operator/controllers/lifecycle/keptntask/job_utils_test.go b/operator/controllers/lifecycle/keptntask/job_utils_test.go index ec242e07e12..5d184173617 100644 --- a/operator/controllers/lifecycle/keptntask/job_utils_test.go +++ b/operator/controllers/lifecycle/keptntask/job_utils_test.go @@ -272,7 +272,7 @@ func makeTaskDefinitionWithConfigmapRef(name, namespace, configMapName string) * }, }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ ConfigMapReference: klcv1alpha3.ConfigMapReference{ Name: configMapName, }, diff --git a/operator/controllers/lifecycle/keptntask/js_builder.go b/operator/controllers/lifecycle/keptntask/js_builder.go index 5a990f4b3c7..b6fb1083e83 100644 --- a/operator/controllers/lifecycle/keptntask/js_builder.go +++ b/operator/controllers/lifecycle/keptntask/js_builder.go @@ -19,8 +19,8 @@ type JSBuilder struct { options BuilderOptions } -func newJSBuilder(options BuilderOptions) JSBuilder { - return JSBuilder{ +func NewJSBuilder(options BuilderOptions) *JSBuilder { + return &JSBuilder{ options: options, } } diff --git a/operator/controllers/lifecycle/keptntask/js_builder_test.go b/operator/controllers/lifecycle/keptntask/js_builder_test.go index b935be9157c..067f235aa11 100644 --- a/operator/controllers/lifecycle/keptntask/js_builder_test.go +++ b/operator/controllers/lifecycle/keptntask/js_builder_test.go @@ -22,7 +22,7 @@ func TestJSBuilder_handleParent(t *testing.T) { Namespace: "default", }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ FunctionReference: klcv1alpha3.FunctionReference{ Name: "mytaskdef", }}}, @@ -33,7 +33,7 @@ func TestJSBuilder_handleParent(t *testing.T) { Namespace: "default", }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ FunctionReference: klcv1alpha3.FunctionReference{ Name: "mytd"}, Parameters: klcv1alpha3.TaskParameters{ @@ -124,7 +124,7 @@ func TestJSBuilder_hasParams(t *testing.T) { Namespace: "default", }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ HttpReference: klcv1alpha3.HttpReference{Url: "donothing"}, Parameters: klcv1alpha3.TaskParameters{ Inline: map[string]string{"DATA2": "mydata2"}, @@ -140,7 +140,7 @@ func TestJSBuilder_hasParams(t *testing.T) { Namespace: "default", }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ HttpReference: klcv1alpha3.HttpReference{Url: "something"}, FunctionReference: klcv1alpha3.FunctionReference{ Name: "mytaskdef"}, diff --git a/operator/controllers/lifecycle/keptntaskdefinition/reconcile_function.go b/operator/controllers/lifecycle/keptntaskdefinition/reconcile_function.go index 321dda631f8..6f1f612e826 100644 --- a/operator/controllers/lifecycle/keptntaskdefinition/reconcile_function.go +++ b/operator/controllers/lifecycle/keptntaskdefinition/reconcile_function.go @@ -18,6 +18,9 @@ import ( ) func (r *KeptnTaskDefinitionReconciler) reconcileFunction(ctx context.Context, req ctrl.Request, definition *klcv1alpha3.KeptnTaskDefinition) error { + if !definition.IsJSSpecDefined() { + return nil + } if definition.Spec.Function.Inline != (klcv1alpha3.Inline{}) { err := r.reconcileFunctionInline(ctx, req, definition) if err != nil { diff --git a/operator/test/component/task/task_test.go b/operator/test/component/task/task_test.go index b86997c5598..883709320dc 100644 --- a/operator/test/component/task/task_test.go +++ b/operator/test/component/task/task_test.go @@ -237,7 +237,7 @@ func makeTaskDefinition(taskDefinitionName, namespace string) *klcv1alpha3.Keptn Namespace: namespace, }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ ConfigMapReference: klcv1alpha3.ConfigMapReference{ Name: cmName, }, diff --git a/operator/test/component/taskdefinition/taskdefinition_test.go b/operator/test/component/taskdefinition/taskdefinition_test.go index adc55b5f84a..da33ac77a63 100644 --- a/operator/test/component/taskdefinition/taskdefinition_test.go +++ b/operator/test/component/taskdefinition/taskdefinition_test.go @@ -41,7 +41,7 @@ var _ = Describe("Taskdefinition", Ordered, func() { Namespace: namespace, }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ Inline: klcv1alpha3.Inline{ Code: "console.log(Hello);", }, @@ -90,7 +90,7 @@ var _ = Describe("Taskdefinition", Ordered, func() { Namespace: namespace, }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ ConfigMapReference: klcv1alpha3.ConfigMapReference{ Name: "my-configmap", }, @@ -141,7 +141,7 @@ var _ = Describe("Taskdefinition", Ordered, func() { Namespace: namespace, }, Spec: klcv1alpha3.KeptnTaskDefinitionSpec{ - Function: klcv1alpha3.FunctionSpec{ + Function: &klcv1alpha3.FunctionSpec{ ConfigMapReference: klcv1alpha3.ConfigMapReference{ Name: "my-configmap-non-existing", }, diff --git a/test/integration/container-runtime/00-assert.yaml b/test/integration/container-runtime/00-assert.yaml new file mode 100644 index 00000000000..109db6ff9fc --- /dev/null +++ b/test/integration/container-runtime/00-assert.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test + name: test +status: + readyReplicas: 1 +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnWorkload +metadata: + name: waiter-waiter +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnWorkloadInstance +metadata: + name: waiter-waiter-0.4 +status: + currentPhase: Completed + deploymentStatus: Succeeded + postDeploymentEvaluationStatus: Succeeded + postDeploymentStatus: Succeeded + preDeploymentEvaluationStatus: Succeeded + preDeploymentStatus: Succeeded + preDeploymentTaskStatus: + - definitionName: pre-deployment-sleep + status: Succeeded + status: Succeeded +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnApp +metadata: + name: waiter +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnAppVersion +metadata: + name: waiter-1b899b6ce1-6b86b273 +status: + currentPhase: Completed + status: Succeeded +--- +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTask +metadata: + annotations: + container: test +status: + status: Succeeded +--- +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + container: test + keptn.sh/app: waiter + keptn.sh/version: '0.4' + keptn.sh/workload: waiter-waiter +status: + conditions: + - type: Complete + status: 'True' + succeeded: 1 diff --git a/test/integration/container-runtime/00-install.yaml b/test/integration/container-runtime/00-install.yaml new file mode 100644 index 00000000000..356aaa942fa --- /dev/null +++ b/test/integration/container-runtime/00-install.yaml @@ -0,0 +1,40 @@ +apiVersion: lifecycle.keptn.sh/v1alpha3 +kind: KeptnTaskDefinition +metadata: + name: pre-deployment-sleep + annotations: + container: test +spec: + container: + name: testy-test + image: busybox:1.36.0 + command: + - 'sh' + - '-c' + - 'sleep 30' +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: test + name: test +spec: + replicas: 1 + selector: + matchLabels: + app: test + strategy: {} + template: + metadata: + labels: + app: test + annotations: + keptn.sh/workload: waiter + keptn.sh/version: "0.4" + keptn.sh/pre-deployment-tasks: pre-deployment-sleep + spec: + containers: + - image: busybox + name: busybox + command: ['sh', '-c', 'echo The app is running! && sleep infinity'] diff --git a/test/integration/container-runtime/00-teststep.yaml b/test/integration/container-runtime/00-teststep.yaml new file mode 100644 index 00000000000..ad4f1d95d54 --- /dev/null +++ b/test/integration/container-runtime/00-teststep.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1 +kind: TestStep +commands: + - script: kubectl annotate ns $NAMESPACE keptn.sh/lifecycle-toolkit='enabled'