Compare commits

...

6 Commits

Author SHA1 Message Date
Nikola Jokic
4de51ee6a5 random handle and random image name 2022-09-21 17:23:16 +02:00
Nikola Jokic
c8e272367f Merge branch 'main' into nikola-jokic/kaniko 2022-09-21 15:32:21 +02:00
Nikola Jokic
c4aa97c974 included generation of random handle/image 2022-09-21 15:29:39 +02:00
Nikola Jokic
f400db92cc Fixed invocation of registry. Basic run works hardcoded
Console logs are left in place and should be deleted
2022-09-21 13:54:25 +02:00
Nikola Jokic
5f0dc3f3b6 created base resource deffinitions for registry and kaniko 2022-09-21 10:39:04 +02:00
Nikola Jokic
6ef042836f fixing defaulting to docker hub on private registry, and b64 encoding 2022-07-29 13:27:17 +02:00
4 changed files with 312 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { v4 as uuidv4 } from 'uuid'
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import { RunContainerStepArgs } from 'hooklib' import { RunContainerStepArgs } from 'hooklib'
import { import {
@@ -8,7 +9,8 @@ import {
getPodLogs, getPodLogs,
getPodStatus, getPodStatus,
waitForJobToComplete, waitForJobToComplete,
waitForPodPhases waitForPodPhases,
containerBuild
} from '../k8s' } from '../k8s'
import { import {
containerVolumes, containerVolumes,
@@ -23,6 +25,8 @@ export async function runContainerStep(
stepContainer: RunContainerStepArgs stepContainer: RunContainerStepArgs
): Promise<number> { ): Promise<number> {
if (stepContainer.dockerfile) { if (stepContainer.dockerfile) {
const imagePath = `${generateBuildHandle()}/${generateBuildTag()}`
await containerBuild(stepContainer, imagePath)
throw new Error('Building container actions is not currently supported') throw new Error('Building container actions is not currently supported')
} }
@@ -108,3 +112,20 @@ function createPodSpec(
return podContainer return podContainer
} }
function generateBuildTag(): string {
return `${generateRandomString()}:${uuidv4().substring(0, 6)}`
}
function generateBuildHandle(): string {
return generateRandomString()
}
function generateRandomString(length = 10): string {
let v = ''
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
for (let i = 0; i < length; i++) {
v += chars.charAt(Math.floor(Math.random() * length))
}
return v
}

View File

@@ -1,6 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import { ContainerInfo, Registry } from 'hooklib' import { RunContainerStepArgs, ContainerInfo, Registry } from 'hooklib'
import * as stream from 'stream' import * as stream from 'stream'
import { import {
getJobPodName, getJobPodName,
@@ -10,6 +10,13 @@ import {
getVolumeClaimName, getVolumeClaimName,
RunnerInstanceLabel RunnerInstanceLabel
} from '../hooks/constants' } from '../hooks/constants'
import {
registryConfigMap,
registrySecret,
registryStatefulSet,
registryService,
kanikoPod
} from './kaniko'
import { PodPhase } from './utils' import { PodPhase } from './utils'
const kc = new k8s.KubeConfig() const kc = new k8s.KubeConfig()
@@ -18,6 +25,7 @@ kc.loadFromDefault()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api) const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
const k8sBatchV1Api = kc.makeApiClient(k8s.BatchV1Api) const k8sBatchV1Api = kc.makeApiClient(k8s.BatchV1Api)
const k8sAppsV1 = kc.makeApiClient(k8s.AppsV1Api)
const k8sAuthorizationV1Api = kc.makeApiClient(k8s.AuthorizationV1Api) const k8sAuthorizationV1Api = kc.makeApiClient(k8s.AuthorizationV1Api)
export const POD_VOLUME_NAME = 'work' export const POD_VOLUME_NAME = 'work'
@@ -52,6 +60,12 @@ export const requiredPermissions = [
verbs: ['create', 'delete', 'get', 'list'], verbs: ['create', 'delete', 'get', 'list'],
resource: 'secrets', resource: 'secrets',
subresource: '' subresource: ''
},
{
group: '',
verbs: ['create', 'delete', 'get', 'list'],
resource: 'configmaps',
subresource: ''
} }
] ]
@@ -326,7 +340,14 @@ export async function waitForPodPhases(
let phase: PodPhase = PodPhase.UNKNOWN let phase: PodPhase = PodPhase.UNKNOWN
try { try {
while (true) { while (true) {
try {
phase = await getPodPhase(podName) phase = await getPodPhase(podName)
} catch (err) {
const e = err as k8s.HttpError
if (e?.body?.reason === 'NotFound') {
phase = PodPhase.UNKNOWN
}
}
if (awaitingPhases.has(phase)) { if (awaitingPhases.has(phase)) {
return return
} }
@@ -464,6 +485,45 @@ export async function isPodContainerAlpine(
return isAlpine return isAlpine
} }
export async function containerBuild(
args: RunContainerStepArgs,
imagePath: string
): Promise<void> {
const cm = registryConfigMap()
const secret = registrySecret()
const ss = registryStatefulSet()
const svc = registryService()
const pod = kanikoPod(args.workingDirectory, imagePath)
await Promise.all([
k8sApi.createNamespacedConfigMap(namespace(), cm),
k8sApi.createNamespacedSecret(namespace(), secret)
])
try {
await k8sAppsV1.createNamespacedStatefulSet(namespace(), ss)
await waitForPodPhases(
'docker-registry-0',
new Set([PodPhase.RUNNING]),
new Set([PodPhase.PENDING, PodPhase.UNKNOWN])
)
} catch (err) {
console.log(err)
console.log(JSON.stringify(err))
throw err
}
try {
await k8sApi.createNamespacedService(namespace(), svc)
} catch (err) {
console.log(JSON.stringify(err))
throw err
}
try {
await k8sApi.createNamespacedPod(namespace(), pod)
} catch (err) {
console.log(JSON.stringify(err))
throw err
}
}
async function getCurrentNodeName(): Promise<string> { async function getCurrentNodeName(): Promise<string> {
const resp = await k8sApi.readNamespacedPod(getRunnerPodName(), namespace()) const resp = await k8sApi.readNamespacedPod(getRunnerPodName(), namespace())

View File

@@ -0,0 +1,208 @@
import * as k8s from '@kubernetes/client-node'
const REGISTRY_CONFIG_MAP_YAML = `
storage:
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
http:
addr: :5000
headers:
X-Content-Type-Options:
- nosniff
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
version: 0.1
`.trim()
export function registryConfigMap(): k8s.V1ConfigMap {
const cm = new k8s.V1ConfigMap()
cm.apiVersion = 'v1'
cm.data = {
'config.yaml': REGISTRY_CONFIG_MAP_YAML
}
cm.kind = 'ConfigMap'
cm.metadata = new k8s.V1ObjectMeta()
cm.metadata.labels = { app: 'docker-registry' }
cm.metadata.name = 'docker-registry-config'
// TODO: make this configurable
return cm
}
export function registrySecret(): k8s.V1Secret {
const secret = new k8s.V1Secret()
secret.apiVersion = 'v1'
secret.data = { haSharedSecret: 'U29tZVZlcnlTdHJpbmdTZWNyZXQK' }
secret.kind = 'Secret'
secret.metadata = new k8s.V1ObjectMeta()
secret.metadata.labels = {
app: 'docker-registry',
chart: 'docker-registry-1.4.3'
}
secret.metadata.name = 'docker-registry-secret'
secret.type = 'Opaque'
return secret
}
export function registryStatefulSet(): k8s.V1StatefulSet {
const ss = new k8s.V1StatefulSet()
ss.apiVersion = 'apps/v1'
ss.metadata = new k8s.V1ObjectMeta()
ss.metadata.name = 'docker-registry'
const spec = new k8s.V1StatefulSetSpec()
spec.selector = new k8s.V1LabelSelector()
spec.selector.matchLabels = { app: 'docker-registry' }
spec.serviceName = 'registry'
spec.replicas = 1
const tmpl = new k8s.V1PodTemplateSpec()
tmpl.metadata = new k8s.V1ObjectMeta()
tmpl.metadata.labels = { app: 'docker-registry' }
tmpl.spec = new k8s.V1PodSpec()
tmpl.spec.terminationGracePeriodSeconds = 5 // TODO: figure out for how long
const c = new k8s.V1Container()
c.command = ['/bin/registry', 'serve', '/etc/docker/registry/config.yaml']
c.env = [
{
name: 'REGISTRY_HTTP_SECRET',
valueFrom: {
secretKeyRef: {
key: 'haSharedSecret',
name: 'docker-registry-secret'
}
}
},
{
name: 'REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY',
value: '/var/lib/registry'
}
]
c.image = 'registry:2.6.2'
c.name = 'docker-registry'
c.imagePullPolicy = 'IfNotPresent'
c.ports = [
{
containerPort: 5000,
protocol: 'TCP'
}
]
c.volumeMounts = [
{
mountPath: '/etc/docker/registry',
name: 'docker-registry-config'
}
]
c.livenessProbe = new k8s.V1Probe()
c.livenessProbe.failureThreshold = 3
c.livenessProbe.periodSeconds = 10
c.livenessProbe.successThreshold = 1
c.livenessProbe.timeoutSeconds = 1
c.livenessProbe.httpGet = new k8s.V1HTTPGetAction()
c.livenessProbe.httpGet.path = '/'
c.livenessProbe.httpGet.port = 5000
c.livenessProbe.httpGet.scheme = 'HTTP'
c.readinessProbe = new k8s.V1Probe()
c.readinessProbe.failureThreshold = 3
c.readinessProbe.periodSeconds = 10
c.readinessProbe.successThreshold = 1
c.readinessProbe.timeoutSeconds = 1
c.readinessProbe.httpGet = new k8s.V1HTTPGetAction()
c.readinessProbe.httpGet.path = '/'
c.readinessProbe.httpGet.port = 5000
c.readinessProbe.httpGet.scheme = 'HTTP'
tmpl.spec.containers = [c]
tmpl.spec.volumes = [
{
name: 'docker-registry-config',
configMap: {
name: 'docker-registry-config'
}
}
]
spec.template = tmpl
ss.spec = spec
return ss
}
export function registryService(): k8s.V1Service {
const svc = new k8s.V1Service()
svc.apiVersion = 'v1'
svc.kind = 'Service'
svc.metadata = new k8s.V1ObjectMeta()
svc.metadata.name = 'docker-registry'
svc.metadata.labels = {
app: 'docker-registry'
}
const spec = new k8s.V1ServiceSpec()
spec.externalTrafficPolicy = 'Cluster'
spec.ports = [
{
name: 'registry',
nodePort: 31500,
port: 5000,
protocol: 'TCP',
targetPort: 5000
}
]
spec.selector = {
app: 'docker-registry'
}
spec.sessionAffinity = 'None'
spec.type = 'NodePort'
svc.spec = spec
return svc
}
export function kanikoPod(
workingDirectory: string, // git://github.com/<handle>/<repo>
imagePath: string // <handle>/<image>:<tag>
): k8s.V1Pod {
const pod = new k8s.V1Pod()
pod.apiVersion = 'v1'
pod.kind = 'Pod'
pod.metadata = new k8s.V1ObjectMeta()
pod.metadata.name = 'kaniko'
const spec = new k8s.V1PodSpec()
const c = new k8s.V1Container()
c.image = 'gcr.io/kaniko-project/executor:latest'
c.name = 'kaniko'
c.imagePullPolicy = 'Always'
c.env = [
{
name: 'GIT_TOKEN',
value: process.env.GITHUB_TOKEN
}
]
c.args = [
'--dockerfile=Dockerfile',
`--context=${workingDirectory}`,
`--destination=docker-registry.default.svc.cluster.local:5000/${imagePath}`
]
spec.containers = [c]
spec.dnsPolicy = 'ClusterFirst'
spec.restartPolicy = 'Never'
pod.spec = spec
return pod
}

View File

@@ -0,0 +1,20 @@
import { containerBuild } from '../src/k8s'
jest.useRealTimers()
describe('container build', () => {
beforeAll(async () => {
process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE'] = 'default'
})
it('should finish without throwing an exception', async () => {
await expect(
containerBuild(
{
workingDirectory: 'git://github.com/nikola-jokic/dockeraction.git'
},
'randhandle/randimg:123123'
)
).resolves.not.toThrow()
})
})