mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-29 22:17:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78838a606d | ||
|
|
fd16fb993d | ||
|
|
1841b7441d | ||
|
|
ce8786b9d2 | ||
|
|
82d366364f | ||
|
|
bd8b2962dc | ||
|
|
f2833f2c4e | ||
|
|
a4c0b0f555 | ||
|
|
a7e30075a9 | ||
|
|
861fd42d29 |
@@ -0,0 +1,24 @@
|
||||
header:
|
||||
license:
|
||||
spdx-id: Apache-2.0
|
||||
copyright-owner: 'FairwindsOps, Inc.'
|
||||
content: |
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
paths:
|
||||
- '**/*.go'
|
||||
comment: on-failure
|
||||
dependency:
|
||||
files:
|
||||
- go.mod
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17.7 AS build-env
|
||||
FROM golang:1.17 AS build-env
|
||||
WORKDIR /go/src/github.com/fairwindsops/polaris/
|
||||
|
||||
ENV GO111MODULE=on
|
||||
@@ -15,7 +15,7 @@ RUN go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
COPY . .
|
||||
RUN packr2 build -a -o polaris *.go
|
||||
|
||||
FROM alpine:3.15.0
|
||||
FROM alpine:3.15.3
|
||||
WORKDIR /usr/local/bin
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
|
||||
@@ -9,3 +9,7 @@ schema:
|
||||
properties:
|
||||
imagePullPolicy:
|
||||
const: Always
|
||||
mutations:
|
||||
- op: add
|
||||
path: /imagePullPolicy
|
||||
value: Always
|
||||
+37
-10
@@ -24,6 +24,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
cfg "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -31,16 +32,20 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var setExitCode bool
|
||||
var onlyShowFailedTests bool
|
||||
var minScore int
|
||||
var auditOutputURL string
|
||||
var auditOutputFile string
|
||||
var auditOutputFormat string
|
||||
var resourceToAudit string
|
||||
var useColor bool
|
||||
var helmChart string
|
||||
var helmValues string
|
||||
var (
|
||||
setExitCode bool
|
||||
onlyShowFailedTests bool
|
||||
minScore int
|
||||
auditOutputURL string
|
||||
auditOutputFile string
|
||||
auditOutputFormat string
|
||||
resourceToAudit string
|
||||
useColor bool
|
||||
helmChart string
|
||||
helmValues string
|
||||
checks []string
|
||||
auditNamespace string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(auditCmd)
|
||||
@@ -56,6 +61,8 @@ func init() {
|
||||
auditCmd.PersistentFlags().StringVar(&resourceToAudit, "resource", "", "Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.")
|
||||
auditCmd.PersistentFlags().StringVar(&helmChart, "helm-chart", "", "Will fill out Helm template")
|
||||
auditCmd.PersistentFlags().StringVar(&helmValues, "helm-values", "", "Optional flag to add helm values")
|
||||
auditCmd.PersistentFlags().StringSliceVar(&checks, "checks", []string{}, "Optional flag to specify specific checks to check")
|
||||
auditCmd.PersistentFlags().StringVar(&auditNamespace, "namespace", "", "Namespace to audit. Only applies to in-cluster audits")
|
||||
}
|
||||
|
||||
var auditCmd = &cobra.Command{
|
||||
@@ -66,6 +73,26 @@ var auditCmd = &cobra.Command{
|
||||
if displayName != "" {
|
||||
config.DisplayName = displayName
|
||||
}
|
||||
if len(checks) > 0 {
|
||||
targetChecks := make(map[string]bool)
|
||||
for _, check := range checks {
|
||||
targetChecks[check] = true
|
||||
}
|
||||
for key := range config.Checks {
|
||||
if isTarget := targetChecks[key]; !isTarget {
|
||||
config.Checks[key] = cfg.SeverityIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
if auditNamespace != "" {
|
||||
if helmChart != "" {
|
||||
logrus.Warn("--namespace and --helm-chart are mutually exclusive. --namespace will be ignored.")
|
||||
}
|
||||
if auditPath != "" {
|
||||
logrus.Warn("--namespace and --audit-path are mutually exclusive. --namespace will be ignored.")
|
||||
}
|
||||
config.Namespace = auditNamespace
|
||||
}
|
||||
if helmChart != "" {
|
||||
var err error
|
||||
auditPath, err = ProcessHelmTemplates(helmChart, helmValues)
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
var configPath string
|
||||
var disallowExemptions, disallowConfigExemptions, disallowAnnotationExemptions bool
|
||||
var disallowExemptions, disallowConfigExemptions, disallowAnnotationExemptions, fixChecks bool
|
||||
var logLevel string
|
||||
var auditPath string
|
||||
var displayName string
|
||||
|
||||
@@ -38,12 +38,14 @@ webhook
|
||||
|
||||
# audit flags
|
||||
--audit-path string If specified, audits one or more YAML files instead of a cluster.
|
||||
--checks stringArray Optional flag to specify specific checks to check
|
||||
--color Whether to use color in pretty format. (default true)
|
||||
--display-name string An optional identifier for the audit.
|
||||
-f, --format string Output format for results - json, yaml, pretty, or score. (default "json")
|
||||
--helm-chart string Will fill out Helm template
|
||||
--helm-values string Optional flag to add helm values
|
||||
-h, --help help for audit
|
||||
--namespace string Namespace to audit. Only applies to in-cluster audits
|
||||
--only-show-failed-tests If specified, audit output will only show failed tests.
|
||||
--output-file string Destination file for audit results.
|
||||
--output-url string Destination URL to send audit results.
|
||||
|
||||
@@ -26,6 +26,7 @@ checks:
|
||||
resourceLimits: warning
|
||||
imageRegistry: danger
|
||||
|
||||
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- my-network-controller
|
||||
@@ -87,3 +88,4 @@ customChecks:
|
||||
not:
|
||||
pattern: ^quay.io
|
||||
|
||||
namespce: test-ns
|
||||
|
||||
@@ -28,6 +28,7 @@ checks:
|
||||
hostPortSet: warning
|
||||
tlsSettingsMissing: warning
|
||||
|
||||
|
||||
exemptions:
|
||||
- namespace: kube-system
|
||||
controllerNames:
|
||||
@@ -201,3 +202,5 @@ exemptions:
|
||||
- kube-hunter
|
||||
rules:
|
||||
- runAsRootAllowed
|
||||
|
||||
namespace: test-ns
|
||||
|
||||
@@ -3,6 +3,8 @@ module github.com/fairwindsops/polaris
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/gobuffalo/packr/v2 v2.8.1
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
@@ -16,6 +18,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/thoas/go-funk v0.9.1
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
|
||||
@@ -158,8 +158,11 @@ github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPO
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs=
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
|
||||
@@ -36,7 +36,9 @@ type Configuration struct {
|
||||
DisallowExemptions bool `json:"disallowExemptions"`
|
||||
DisallowConfigExemptions bool `json:"disallowConfigExemptions"`
|
||||
DisallowAnnotationExemptions bool `json:"disallowAnnotationExemptions"`
|
||||
Mutations []string `json:"mutations"`
|
||||
KubeContext string `json:"kubeContext"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// Exemption represents an exemption to normal rules
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -51,6 +65,7 @@ type SchemaCheck struct {
|
||||
AdditionalSchemas map[string]map[string]interface{} `yaml:"additionalSchemas" json:"additionalSchemas"`
|
||||
AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
|
||||
AdditionalValidators map[string]jsonschema.RootSchema `yaml:"-" json:"-"`
|
||||
Mutations []map[string]interface{} `yaml:"mutations" json:"mutations"`
|
||||
}
|
||||
|
||||
type resourceMinimum string
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
@@ -238,4 +252,3 @@ func TestStringInSlice(t *testing.T) {
|
||||
assert.Equal(t, expectedOutput, actual)
|
||||
assert.NotEqual(t, true, actual)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kube
|
||||
|
||||
import (
|
||||
|
||||
+49
-12
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kube
|
||||
|
||||
import (
|
||||
@@ -239,19 +253,38 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
|
||||
logrus.Errorf("Error fetching Cluster API version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Cluster", clusterName)
|
||||
|
||||
sourceType := "Cluster"
|
||||
if c.Namespace != "" {
|
||||
logrus.Debug("namespace is specififed in config, setting source type to ClusterNamespace")
|
||||
sourceType = "ClusterNamespace"
|
||||
}
|
||||
provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, sourceType, clusterName)
|
||||
|
||||
nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Nodes: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
namespaces, err := kube.CoreV1().Namespaces().List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Namespaces: %v", err)
|
||||
return nil, err
|
||||
|
||||
var namespaces *corev1.NamespaceList
|
||||
if c.Namespace != "" {
|
||||
ns, err := kube.CoreV1().Namespaces().Get(ctx, c.Namespace, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespaces = &corev1.NamespaceList{
|
||||
Items: []corev1.Namespace{*ns},
|
||||
}
|
||||
} else {
|
||||
nsList, err := kube.CoreV1().Namespaces().List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Namespaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
namespaces = nsList
|
||||
}
|
||||
pods, err := kube.CoreV1().Pods("").List(ctx, listOpts)
|
||||
pods, err := kube.CoreV1().Pods(c.Namespace).List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Pods: %v", err)
|
||||
return nil, err
|
||||
@@ -287,6 +320,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
|
||||
}
|
||||
}
|
||||
|
||||
var kubernetesResources []GenericResource
|
||||
for _, kind := range additionalKinds {
|
||||
groupKind := parseGroupKind(maybeTransformKindIntoGroupKind(string(kind)))
|
||||
mapping, err := (restMapper).RESTMapping(groupKind)
|
||||
@@ -295,7 +329,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objects, err := (*dynamic).Resource(mapping.Resource).Namespace("").List(ctx, metav1.ListOptions{})
|
||||
objects, err := (*dynamic).Resource(mapping.Resource).Namespace(c.Namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
|
||||
return nil, err
|
||||
@@ -305,7 +339,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider.Resources.addResource(res)
|
||||
kubernetesResources = append(kubernetesResources, res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,9 +350,12 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
|
||||
logrus.Errorf("Error loading controllers from pods: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// resources loaded from custom checks can also contain controllers and thus would be added twice to the provider
|
||||
kubernetesResources = deduplicateControllers(append(kubernetesResources, controllers...))
|
||||
|
||||
provider.Nodes = nodes.Items
|
||||
provider.Namespaces = namespaces.Items
|
||||
provider.Resources.addResources(controllers)
|
||||
provider.Resources.addResources(kubernetesResources)
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
@@ -341,14 +378,14 @@ func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointe
|
||||
}
|
||||
interfaces = append(interfaces, workload)
|
||||
}
|
||||
return deduplicateControllers(interfaces), nil
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
|
||||
// In cases like CronJobs older children can hang around, so this takes the most recent.
|
||||
func deduplicateControllers(inputControllers []GenericResource) []GenericResource {
|
||||
func deduplicateControllers(inputResources []GenericResource) []GenericResource {
|
||||
controllerMap := make(map[string]GenericResource)
|
||||
for _, controller := range inputControllers {
|
||||
for _, controller := range inputResources {
|
||||
key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName()
|
||||
oldController, ok := controllerMap[key]
|
||||
if !ok || controller.ObjectMeta.GetCreationTimestamp().Time.After(oldController.ObjectMeta.GetCreationTimestamp().Time) {
|
||||
|
||||
+76
-15
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kube
|
||||
|
||||
import (
|
||||
@@ -83,15 +97,6 @@ func TestAddResourcesFromReader(t *testing.T) {
|
||||
|
||||
func TestGetResourceFromAPI(t *testing.T) {
|
||||
k8s, dynamicInterface := test.SetupTestAPI(test.GetMockControllers("test")...)
|
||||
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface, conf.Configuration{})
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
|
||||
assert.Equal(t, "test", resources.SourceName, "Should have source name")
|
||||
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
|
||||
|
||||
expectedNames := map[string]bool{
|
||||
"deploy": false,
|
||||
@@ -100,12 +105,68 @@ func TestGetResourceFromAPI(t *testing.T) {
|
||||
"statefulset": false,
|
||||
"daemonset": false,
|
||||
}
|
||||
for _, controllers := range resources.Resources {
|
||||
for _, ctrl := range controllers {
|
||||
expectedNames[ctrl.ObjectMeta.GetName()] = true
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config conf.Configuration
|
||||
want *ResourceProvider
|
||||
wantErr bool
|
||||
clusterName string
|
||||
}{
|
||||
{
|
||||
name: "standard",
|
||||
config: conf.Configuration{},
|
||||
clusterName: "test1",
|
||||
want: &ResourceProvider{
|
||||
SourceType: "Cluster",
|
||||
SourceName: "test1",
|
||||
CreationTime: time.Now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespaced",
|
||||
config: conf.Configuration{
|
||||
Namespace: "test",
|
||||
},
|
||||
clusterName: "test2",
|
||||
want: &ResourceProvider{
|
||||
SourceType: "ClusterNamespace",
|
||||
SourceName: "test2",
|
||||
CreationTime: time.Now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespace does not exist",
|
||||
config: conf.Configuration{
|
||||
Namespace: "test3",
|
||||
},
|
||||
clusterName: "test3",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for name, val := range expectedNames {
|
||||
assert.Equal(t, true, val, name)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, &dynamicInterface, tt.config)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want.SourceType, resources.SourceType)
|
||||
assert.Equal(t, tt.want.SourceName, resources.SourceName)
|
||||
assert.IsType(t, tt.want.CreationTime, resources.CreationTime)
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
|
||||
|
||||
for _, controllers := range resources.Resources {
|
||||
for _, ctrl := range controllers {
|
||||
expectedNames[ctrl.ObjectMeta.GetName()] = true
|
||||
}
|
||||
}
|
||||
for name, val := range expectedNames {
|
||||
assert.Equal(t, true, val, name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
)
|
||||
|
||||
// ApplyAllSchemaMutations applies available mutation to a single resource
|
||||
func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource, mutations []map[string]interface{}) (kube.GenericResource, error) {
|
||||
resByte := resource.OriginalObjectJSON
|
||||
var jsonByte []byte
|
||||
mutationByte, err := json.Marshal(mutations)
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
patch, err := jsonpatch.DecodePatch(mutationByte)
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
jsonByte, err = patch.Apply(resByte)
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
mutated, err := kube.NewGenericResourceFromBytes(jsonByte)
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
return mutated, nil
|
||||
}
|
||||
|
||||
// GetMutationsFromResults returns all mutations from results
|
||||
func GetMutationsFromResults(conf *config.Configuration, results []validator.Result) map[string][]map[string]interface{} {
|
||||
allMutationsFromResults := make(map[string][]map[string]interface{})
|
||||
for _, result := range results {
|
||||
key := fmt.Sprintf("%s/%s/%s", result.Kind, result.Name, result.Namespace)
|
||||
|
||||
for _, resultMessage := range result.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations, ok := allMutationsFromResults[key]
|
||||
if !ok {
|
||||
mutations = make([]map[string]interface{}, 0)
|
||||
}
|
||||
allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, resultMessage := range result.PodResult.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations, ok := allMutationsFromResults[key]
|
||||
if !ok {
|
||||
mutations = make([]map[string]interface{}, 0)
|
||||
}
|
||||
allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, containerResult := range result.PodResult.ContainerResults {
|
||||
for _, resultMessage := range containerResult.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations, ok := allMutationsFromResults[key]
|
||||
if !ok {
|
||||
mutations = make([]map[string]interface{}, 0)
|
||||
}
|
||||
allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return allMutationsFromResults
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
|
||||
@@ -72,12 +72,13 @@ type ClusterInfo struct {
|
||||
|
||||
// ResultMessage is the result of a given check
|
||||
type ResultMessage struct {
|
||||
ID string
|
||||
Message string
|
||||
Details []string
|
||||
Success bool
|
||||
Severity config.Severity
|
||||
Category string
|
||||
ID string
|
||||
Message string
|
||||
Details []string
|
||||
Success bool
|
||||
Severity config.Severity
|
||||
Category string
|
||||
Mutations []map[string]interface{}
|
||||
}
|
||||
|
||||
// ResultSet contiains the results for a set of checks
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/thoas/go-funk"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -239,6 +255,7 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
}
|
||||
var passes bool
|
||||
var issues []jsonschema.ValError
|
||||
var prefix string
|
||||
if check.SchemaTarget != "" {
|
||||
if check.SchemaTarget == config.TargetPod && check.Target == config.TargetContainer {
|
||||
podCopy := *test.Resource.PodSpec
|
||||
@@ -250,7 +267,15 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
}
|
||||
} else if check.Target == config.TargetPod {
|
||||
passes, issues, err = check.CheckPod(test.Resource.PodSpec)
|
||||
prefix = getJSONSchemaPrefix(test.Resource.Kind)
|
||||
} else if check.Target == config.TargetContainer {
|
||||
containerIndex := funk.IndexOf(test.Resource.PodSpec.Containers, func(value corev1.Container) bool {
|
||||
return value.Name == test.Container.Name
|
||||
})
|
||||
prefix = getJSONSchemaPrefix(test.Resource.Kind)
|
||||
if prefix != "" {
|
||||
prefix += "/containers/" + strconv.Itoa(containerIndex)
|
||||
}
|
||||
passes, issues, err = check.CheckContainer(test.Container)
|
||||
} else {
|
||||
passes, issues, err = check.CheckObject(test.Resource.Resource.Object)
|
||||
@@ -279,6 +304,16 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
}
|
||||
}
|
||||
result := makeResult(conf, check, passes, issues)
|
||||
if !passes {
|
||||
if funk.Contains(conf.Mutations, checkID) {
|
||||
mutations := funk.Map(check.Mutations, func(mutation map[string]interface{}) map[string]interface{} {
|
||||
mutationCopy := deepCopyMutation(mutation)
|
||||
mutationCopy["path"] = prefix + mutationCopy["path"].(string)
|
||||
return mutationCopy
|
||||
}).([]map[string]interface{})
|
||||
result.Mutations = mutations
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -290,3 +325,25 @@ func getSortedKeys(m map[string]config.Severity) []string {
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func deepCopyMutation(source map[string]interface{}) map[string]interface{} {
|
||||
destination := map[string]interface{}{}
|
||||
for key, value := range source {
|
||||
destination[key] = value
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
func getJSONSchemaPrefix(kind string) (prefix string) {
|
||||
if kind == "CronJob" {
|
||||
prefix = "/spec/jobTemplate/spec/template/spec"
|
||||
} else if kind == "Pod" {
|
||||
prefix = "/spec"
|
||||
} else if (kind == "Deployment") || (kind == "Daemonset") ||
|
||||
(kind == "Statefulset") || (kind == "Job") || (kind == "ReplicationController") {
|
||||
prefix = "/spec/template/spec"
|
||||
} else {
|
||||
logrus.Warningf("Mutation for this this resource (%s) is not supported", kind)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: hello
|
||||
spec:
|
||||
schedule: "* * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: hello
|
||||
image: busybox
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- date; echo Hello from the Kubernetes cluster
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: hello
|
||||
spec:
|
||||
schedule: "* * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: hello
|
||||
image: busybox
|
||||
imagePullPolicy: Always
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- date; echo Hello from the Kubernetes cluster
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
+35
-1
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
@@ -88,7 +102,10 @@ func MockController(apiVersion, kind, namespace, name string, spec map[string]in
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
json.Unmarshal(b, &dest)
|
||||
err = json.Unmarshal(b, &dest)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pod
|
||||
}
|
||||
|
||||
@@ -158,6 +175,15 @@ func MockReplicationController(namespace, name string) (corev1.ReplicationContro
|
||||
return rc, pod
|
||||
}
|
||||
|
||||
// MockNamespace returns a namespace object.
|
||||
func MockNamespace(name string) corev1.Namespace {
|
||||
return corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTestAPI creates a test kube API struct.
|
||||
func SetupTestAPI(objects ...runtime.Object) (kubernetes.Interface, dynamic.Interface) {
|
||||
scheme := runtime.NewScheme()
|
||||
@@ -221,12 +247,19 @@ func SetupTestAPI(objects ...runtime.Object) (kubernetes.Interface, dynamic.Inte
|
||||
{Name: "poddisruptionbudgets", Namespaced: true, Kind: "PodDisruptionBudget", Version: "v1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
GroupVersion: "core/v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{Name: "namespaces", Namespaced: false, Kind: "Namespace"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return k, dynamicClient
|
||||
}
|
||||
|
||||
// GetMockControllers returns mocked controllers for 5 major controller types
|
||||
func GetMockControllers(namespace string) []runtime.Object {
|
||||
ns := MockNamespace(namespace)
|
||||
deploy, deployPod := MockDeploy(namespace, "deploy")
|
||||
statefulset, statefulsetPod := MockStatefulSet(namespace, "statefulset")
|
||||
daemonset, daemonsetPod := MockDaemonSet(namespace, "daemonset")
|
||||
@@ -238,5 +271,6 @@ func GetMockControllers(namespace string) []runtime.Object {
|
||||
&statefulset, &statefulsetPod,
|
||||
&cronjob, &cronjobPod,
|
||||
&job, &jobPod,
|
||||
&ns,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/mutation"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
var configYaml = `
|
||||
checks:
|
||||
pullPolicyNotAlways: warning
|
||||
mutations:
|
||||
- pullPolicyNotAlways
|
||||
`
|
||||
|
||||
func TestMutations(t *testing.T) {
|
||||
c, err := config.Parse([]byte(configYaml))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, c.Mutations, 1)
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.failure && funk.Contains(c.Mutations, tc.check) {
|
||||
key := fmt.Sprintf("%s/%s", tc.check, strings.ReplaceAll(tc.filename, "failure", "success"))
|
||||
successResources, ok := successResourceMap[key]
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, tc.resources.Resources, 1)
|
||||
assert.Len(t, successResources.Resources, 1)
|
||||
results, err := validator.ApplyAllSchemaChecksToResourceProvider(&c, tc.resources)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
allMutations := mutation.GetMutationsFromResults(&c, results)
|
||||
assert.Len(t, allMutations, 1)
|
||||
for kind, resources := range tc.resources.Resources {
|
||||
key := fmt.Sprintf("%s/%s/%s", resources[0].Kind, resources[0].Resource.GetName(), resources[0].Resource.GetNamespace())
|
||||
mutations := allMutations[key]
|
||||
assert.Len(t, mutations, 1)
|
||||
mutated, err := mutation.ApplyAllSchemaMutations(&c, tc.resources, resources[0], mutations)
|
||||
assert.NoError(t, err)
|
||||
expected := successResources.Resources[kind][0]
|
||||
assert.Equal(t, expected.Resource.Object, mutated.Resource.Object)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2022 FairwindsOps, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
@@ -24,6 +38,8 @@ type testCase struct {
|
||||
failure bool
|
||||
}
|
||||
|
||||
var successResourceMap = map[string]*kube.ResourceProvider{}
|
||||
|
||||
func init() {
|
||||
_, baseDir, _, _ := runtime.Caller(0)
|
||||
baseDir = filepath.Dir(baseDir) + "/checks"
|
||||
@@ -49,6 +65,10 @@ func init() {
|
||||
resources: resources,
|
||||
failure: strings.Contains(tc.Name(), "failure"),
|
||||
})
|
||||
if !strings.Contains(tc.Name(), "failure") {
|
||||
key := fmt.Sprintf("%s/%s", check, tc.Name())
|
||||
successResourceMap[key] = resources
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user