github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/k8s/environment/runner.go (about) 1 package environment 2 3 import ( 4 "fmt" 5 "os" 6 "strconv" 7 "strings" 8 9 "github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2" 10 "github.com/rs/zerolog/log" 11 12 "github.com/smartcontractkit/chainlink-testing-framework/libs/k8s/config" 13 "github.com/smartcontractkit/chainlink-testing-framework/libs/k8s/imports/k8s" 14 a "github.com/smartcontractkit/chainlink-testing-framework/libs/k8s/pkg/alias" 15 "github.com/smartcontractkit/chainlink-testing-framework/libs/utils/ptr" 16 ) 17 18 const REMOTE_RUNNER_NAME = "remote-test-runner" 19 20 type Chart struct { 21 Props *Props 22 } 23 24 func (m Chart) IsDeploymentNeeded() bool { 25 return true 26 } 27 28 func (m Chart) GetName() string { 29 return REMOTE_RUNNER_NAME 30 } 31 32 func (m Chart) GetProps() interface{} { 33 return m.Props 34 } 35 36 func (m Chart) GetPath() string { 37 return "" 38 } 39 40 func (m Chart) GetVersion() string { 41 return "" 42 } 43 44 func (m Chart) GetValues() *map[string]interface{} { 45 return nil 46 } 47 48 func (m Chart) ExportData(e *Environment) error { 49 return nil 50 } 51 52 func NewRunner(props *Props) func(root cdk8s.Chart) ConnectedChart { 53 return func(root cdk8s.Chart) ConnectedChart { 54 c := &Chart{ 55 Props: props, 56 } 57 role(root, props) 58 job(root, props) 59 return c 60 } 61 } 62 63 type Props struct { 64 BaseName string 65 TargetNamespace string 66 Labels *map[string]*string 67 Image string 68 TestName string 69 NoManifestUpdate bool 70 PreventPodEviction bool 71 } 72 73 func role(chart cdk8s.Chart, props *Props) { 74 k8s.NewKubeRole( 75 chart, 76 ptr.Ptr(fmt.Sprintf("%s-role", props.BaseName)), 77 &k8s.KubeRoleProps{ 78 Metadata: &k8s.ObjectMeta{ 79 Name: ptr.Ptr(props.BaseName), 80 }, 81 Rules: &[]*k8s.PolicyRule{ 82 { 83 ApiGroups: &[]*string{ 84 ptr.Ptr(""), // this empty line is needed or k8s get really angry 85 ptr.Ptr("apps"), 86 ptr.Ptr("batch"), 87 ptr.Ptr("core"), 88 ptr.Ptr("networking.k8s.io"), 89 ptr.Ptr("storage.k8s.io"), 90 ptr.Ptr("policy"), 91 ptr.Ptr("chaos-mesh.org"), 92 ptr.Ptr("monitoring.coreos.com"), 93 ptr.Ptr("rbac.authorization.k8s.io"), 94 }, 95 Resources: &[]*string{ 96 ptr.Ptr("*"), 97 }, 98 Verbs: &[]*string{ 99 ptr.Ptr("*"), 100 }, 101 }, 102 }, 103 }) 104 k8s.NewKubeRoleBinding( 105 chart, 106 ptr.Ptr(fmt.Sprintf("%s-role-binding", props.BaseName)), 107 &k8s.KubeRoleBindingProps{ 108 RoleRef: &k8s.RoleRef{ 109 ApiGroup: ptr.Ptr("rbac.authorization.k8s.io"), 110 Kind: ptr.Ptr("Role"), 111 Name: ptr.Ptr("remote-test-runner"), 112 }, 113 Metadata: nil, 114 Subjects: &[]*k8s.Subject{ 115 { 116 Kind: ptr.Ptr("ServiceAccount"), 117 Name: ptr.Ptr("default"), 118 Namespace: ptr.Ptr(props.TargetNamespace), 119 }, 120 }, 121 }, 122 ) 123 } 124 125 func job(chart cdk8s.Chart, props *Props) { 126 defaultRunnerPodAnnotations := markNotSafeToEvict(props.PreventPodEviction, nil) 127 restartPolicy := "Never" 128 backOffLimit := float64(0) 129 if os.Getenv(config.EnvVarDetachRunner) == "true" { // If we're running detached, we're likely running a long-form test 130 restartPolicy = "OnFailure" 131 backOffLimit = 100000 // effectively infinite (I hope) 132 } 133 k8s.NewKubeJob( 134 chart, 135 ptr.Ptr(fmt.Sprintf("%s-job", props.BaseName)), 136 &k8s.KubeJobProps{ 137 Metadata: &k8s.ObjectMeta{ 138 Name: ptr.Ptr(props.BaseName), 139 }, 140 Spec: &k8s.JobSpec{ 141 Template: &k8s.PodTemplateSpec{ 142 Metadata: &k8s.ObjectMeta{ 143 Labels: props.Labels, 144 Annotations: a.ConvertAnnotations(defaultRunnerPodAnnotations), 145 }, 146 Spec: &k8s.PodSpec{ 147 ServiceAccountName: ptr.Ptr("default"), 148 Containers: &[]*k8s.Container{ 149 container(props), 150 }, 151 RestartPolicy: ptr.Ptr(restartPolicy), 152 Volumes: &[]*k8s.Volume{ 153 { 154 Name: ptr.Ptr("persistence"), 155 EmptyDir: &k8s.EmptyDirVolumeSource{}, 156 }, 157 }, 158 }, 159 }, 160 ActiveDeadlineSeconds: nil, 161 BackoffLimit: ptr.Ptr(backOffLimit), 162 }, 163 }) 164 } 165 166 func container(props *Props) *k8s.Container { 167 cpu := os.Getenv(config.EnvVarRemoteRunnerCpu) 168 if cpu == "" { 169 cpu = "1000m" 170 } 171 mem := os.Getenv(config.EnvVarRemoteRunnerMem) 172 if mem == "" { 173 mem = "1024Mi" 174 } 175 return &k8s.Container{ 176 Name: ptr.Ptr(fmt.Sprintf("%s-node", props.BaseName)), 177 Image: ptr.Ptr(props.Image), 178 ImagePullPolicy: ptr.Ptr("Always"), 179 Env: jobEnvVars(props), 180 Resources: a.ContainerResources(cpu, mem, cpu, mem), 181 VolumeMounts: &[]*k8s.VolumeMount{ 182 { 183 Name: ptr.Ptr("persistence"), 184 MountPath: ptr.Ptr("/persistence"), 185 }, 186 }, 187 } 188 } 189 190 func jobEnvVars(props *Props) *[]*k8s.EnvVar { 191 // Use a map to set values so we can easily overwrite duplicate values 192 env := make(map[string]string) 193 194 // Propagate common environment variables to the runner 195 lookups := []string{ 196 config.EnvVarCLImage, 197 config.EnvVarCLTag, 198 config.EnvVarCLCommitSha, 199 config.EnvVarLogLevel, 200 config.EnvVarTestTrigger, 201 config.EnvVarToleration, 202 config.EnvVarSlackChannel, 203 config.EnvVarSlackKey, 204 config.EnvVarSlackUser, 205 config.EnvVarUser, 206 config.EnvVarNodeSelector, 207 config.EnvVarDBURL, 208 config.EnvVarInternalDockerRepo, 209 config.EnvVarLocalCharts, 210 config.EnvBase64ConfigOverride, 211 config.EnvBase64NetworkConfig, 212 } 213 for _, k := range lookups { 214 v, success := os.LookupEnv(k) 215 if success && len(v) > 0 { 216 log.Debug().Str(k, v).Msg("Forwarding Env Var") 217 env[k] = v 218 } 219 } 220 221 // Propagate prefixed variables to the runner 222 // These should overwrite anything that was unprefixed if they match up 223 for _, e := range os.Environ() { 224 if i := strings.Index(e, "="); i >= 0 { 225 if strings.HasPrefix(e[:i], config.EnvVarPrefix) { 226 withoutPrefix := strings.Replace(e[:i], config.EnvVarPrefix, "", 1) 227 log.Debug().Str(e[:i], e[i+1:]).Msg("Forwarding generic Env Var") 228 env[withoutPrefix] = e[i+1:] 229 } 230 } 231 } 232 233 // Add variables that should need specific values for thre remote runner 234 env[config.EnvVarNamespace] = props.TargetNamespace 235 env["TEST_NAME"] = props.TestName 236 env[config.EnvVarInsideK8s] = "true" 237 env[config.EnvVarNoManifestUpdate] = strconv.FormatBool(props.NoManifestUpdate) 238 239 // convert from map to the expected array 240 cdk8sVars := make([]*k8s.EnvVar, 0) 241 for k, v := range env { 242 cdk8sVars = append(cdk8sVars, a.EnvVarStr(k, v)) 243 } 244 return &cdk8sVars 245 }