This commit is contained in:
2024-02-20 17:15:27 +08:00
committed by huty
parent 6706e1a633
commit 34158042ad
1529 changed files with 177765 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
version: "3.7"
services:
ch07-timecheck:
image: kiamol/ch07-timecheck:latest-linux-amd64
ch07-simple-proxy:
image: kiamol/ch07-simple-proxy:latest-linux-amd64

View File

@@ -0,0 +1,8 @@
version: "3.7"
services:
ch07-timecheck:
image: kiamol/ch07-timecheck:latest-linux-arm64
ch07-simple-proxy:
image: kiamol/ch07-simple-proxy:latest-linux-arm64

View File

@@ -0,0 +1,12 @@
version: "3.6"
services:
ch07-timecheck:
image: kiamol/ch07-timecheck:latest
build:
context: ./timecheck
ch07-simple-proxy:
image: kiamol/ch07-simple-proxy:latest
build:
context: ./simple-proxy

View File

@@ -0,0 +1,10 @@
$images=$(yq e '.services.[].image' docker-compose.yml)
foreach ($image in $images)
{
docker manifest create --amend $image `
"$($image)-linux-arm64" `
"$($image)-linux-amd64"
docker manifest push $image
}

View File

@@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS builder
WORKDIR /src
COPY src/SimpleProxy.csproj .
RUN dotnet restore
COPY src /src
RUN dotnet publish -c Release -o /out SimpleProxy.csproj
# app image
FROM mcr.microsoft.com/dotnet/runtime:6.0-alpine
EXPOSE 1080
WORKDIR /app
ENTRYPOINT ["dotnet", "SimpleProxy.dll"]
COPY --from=builder /out/ .

View File

@@ -0,0 +1,65 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Titanium.Web.Proxy;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
namespace SimpleProxy
{
class Program
{
private static ManualResetEvent _ResetEvent = new ManualResetEvent(false);
private static IConfiguration _Config;
static void Main(string[] args)
{
_Config = GetConfig();
var port = _Config.GetValue<int>("Proxy:Port");
var proxyServer = new ProxyServer();
proxyServer.BeforeRequest += OnRequest;
var explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, port, false);
proxyServer.AddEndPoint(explicitEndPoint);
proxyServer.Start();
Console.WriteLine($"** Logging proxy listening on port: {port} **");
_ResetEvent.WaitOne();
proxyServer.BeforeRequest -= OnRequest;
proxyServer.Stop();
}
private static IConfiguration GetConfig()
{
return new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
}
public static Task OnRequest(object sender, SessionEventArgs e)
{
var sourceUri = e.HttpClient.Request.RequestUri.AbsoluteUri;
if (sourceUri == _Config.GetValue<string>("Proxy:Request:UriMap:Source"))
{
var targetUri = _Config.GetValue<string>("Proxy:Request:UriMap:Target");
Console.WriteLine($"{e.HttpClient.Request.Method} {sourceUri} -> {targetUri}");
e.HttpClient.Request.RequestUri = new Uri(targetUri);
}
else if (_Config.GetValue<bool>("Proxy:Request:RejectUnknown"))
{
Console.WriteLine($"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url} [BLOCKED]");
e.Ok("");
}
else
{
Console.WriteLine($"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url}");
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Titanium.Web.Proxy" Version="3.1.1301" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"Proxy": {
"Port": 1080,
"Request": {
"RejectUnknown": true,
"UriMap": {
"Source": "http://localhost/api",
"Target": "http://blog.sixeyed.com"
}
}
}
}

View File

@@ -0,0 +1,18 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS builder
WORKDIR /src
COPY src/TimeCheck.csproj .
RUN dotnet restore
COPY src /src
RUN dotnet publish -c Release -o /out TimeCheck.csproj
# app image
FROM mcr.microsoft.com/dotnet/runtime:6.0-alpine
COPY src/appsettings.json /config/appsettings.json
WORKDIR /app
ENTRYPOINT ["dotnet", "TimeCheck.dll"]
COPY --from=builder /out/ .

View File

@@ -0,0 +1,68 @@
using System;
using System.Threading;
using System.Timers;
using Microsoft.Extensions.Configuration;
using Prometheus;
using Serilog;
namespace Kiamol.Ch07.TimeCheck
{
class Program
{
private static readonly ManualResetEvent _ResetEvent = new ManualResetEvent(false);
private static Counter _CheckCounter;
private static string _Version;
private static string _Env;
public static void Main()
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile("/config/appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
_Version = config["Application:Version"];
_Env = config["Application:Environment"];
var intervalSeconds = config.GetValue<int>("Timer:IntervalSeconds") * 1000;
var metricsEnabled = config.GetValue<bool>("Metrics:Enabled");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File("/logs/timecheck.log", shared: true, flushToDiskInterval: TimeSpan.FromSeconds(intervalSeconds))
.CreateLogger();
if (metricsEnabled)
{
_CheckCounter = Metrics.CreateCounter("timecheck_total", "Number of timechecks");
StartMetricServer(config);
}
using (var timer = new System.Timers.Timer(intervalSeconds))
{
timer.Elapsed += WriteTimeCheck;
timer.Enabled = true;
_ResetEvent.WaitOne();
}
}
private static void WriteTimeCheck(Object source, ElapsedEventArgs e)
{
Log.Information("Environment: {environment}; version: {version}; time check: {timestamp}",
_Env, _Version, e.SignalTime.ToString("HH:mm.ss"));
if (_CheckCounter != null)
{
_CheckCounter.Inc();
}
}
private static void StartMetricServer(IConfiguration config)
{
var metricsPort = config.GetValue<int>("Metrics:Port");
var server = new MetricServer(metricsPort);
server.Start();
Log.Information("Metrics server listening on port: {metricsPort}", metricsPort);
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="prometheus-net" Version="3.6.0" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30204.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeCheck", "TimeCheck.csproj", "{FC0AF23D-2E33-4AE5-8ED8-E8F228AA00B0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FC0AF23D-2E33-4AE5-8ED8-E8F228AA00B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC0AF23D-2E33-4AE5-8ED8-E8F228AA00B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC0AF23D-2E33-4AE5-8ED8-E8F228AA00B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC0AF23D-2E33-4AE5-8ED8-E8F228AA00B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {502A678D-1443-4853-A777-00FA08C2BF53}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,13 @@
{
"Application": {
"Version": "1.0",
"Environment": "DEV"
},
"Timer": {
"IntervalSeconds": "5"
},
"Metrics": {
"Enabled": false,
"Port" : 8080
}
}

View File

@@ -0,0 +1,53 @@
# Ch07 lab
Run the app:
```
kubectl apply -f lab/pi/
```
Check the app and see it's broken:
```
kubectl describe pod -l app=pi-web
```
> The startup command for the app container uses a script which doesn't exist.
## Sample Solution
My updated [Deployment](solution/web.yaml) for the web app uses multiple containers.
Init containers:
- init container `init-1` writes the startup script file in an EmptyDir volume
- `init-2` makes the startup script executable
- `init-3` writes a text file with a fake app version.
App container:
- mounts the EmptyDir volume and runs the script at startup; serves the app on port 80.
Sidecar:
- runs a simple NCat HTTP server, serving the version number text file on port 8080.
Run the update and browse to your Service on port 8070 for Pi and 8071 for the version:
```
kubectl apply -f lab/solution/
```
> This is not the most efficient way to do this! It's just an example which makes use of multi-container Pods.
## Teardown
Delete the lab resources by their labels:
```
kubectl get all -l kiamol=ch07-lab
kubectl delete all -l kiamol=ch07-lab
```

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: pi-web
labels:
kiamol: ch07-lab
spec:
ports:
- port: 8070
targetPort: 80
name: http
selector:
app: pi-web
type: LoadBalancer

View File

@@ -0,0 +1,23 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pi-web
labels:
kiamol: ch07-lab
spec:
replicas: 2
selector:
matchLabels:
app: pi-web
template:
metadata:
labels:
app: pi-web
spec:
containers:
- image: kiamol/ch05-pi
command: ["/scripts/startup.sh"]
name: web
ports:
- containerPort: 80
name: http

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: pi-web-version
labels:
kiamol: ch07-lab
spec:
ports:
- port: 8071
targetPort: 8080
name: http
selector:
app: pi-web
type: LoadBalancer

View File

@@ -0,0 +1,58 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pi-web
labels:
kiamol: ch07-lab
spec:
replicas: 2
selector:
matchLabels:
app: pi-web
template:
metadata:
labels:
app: pi-web
spec:
initContainers:
- name: init-script-1
image: kiamol/ch03-sleep
command: ['sh', '-c', "echo '#!/bin/sh\ndotnet Pi.Web.dll -m web' > /init/startup.sh"]
volumeMounts:
- name: init
mountPath: "/init"
- name: init-script-2
image: kiamol/ch03-sleep
command: ['sh', '-c', "chmod +x /init/startup.sh"]
volumeMounts:
- name: init
mountPath: "/init"
- name: init-version
image: kiamol/ch03-sleep
command: ['sh', '-c', "echo 'ch07-lab' > /init/version.txt"]
volumeMounts:
- name: init
mountPath: "/init"
containers:
- name: web
image: kiamol/ch05-pi
command: ["/init/startup.sh"]
ports:
- containerPort: 80
name: http
volumeMounts:
- name: init
mountPath: "/init"
- name: server
image: kiamol/ch03-sleep
command: ['sh', '-c', 'while true; do echo -e "HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 9\n\n$(cat /init/version.txt)" | nc -l -p 8080; done']
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: init
mountPath: "/init"
volumes:
- name: init
emptyDir: {}

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: numbers-api
labels:
kiamol: ch07
spec:
ports:
- port: 80
selector:
app: numbers-api
type: ClusterIP

View File

@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: numbers-api
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: numbers-api
template:
metadata:
labels:
app: numbers-api
spec:
containers:
- name: api
image: kiamol/ch03-numbers-api

View File

@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: numbers-web
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: numbers-web
template:
metadata:
labels:
app: numbers-web
version: v2
spec:
initContainers:
- name: init-version
image: kiamol/ch03-sleep
command: ['sh', '-c', "echo v2 > /config-out/version.txt"]
env:
- name: APP_ENVIRONMENT
value: TEST
volumeMounts:
- name: config-dir
mountPath: /config-out
readOnly: true
containers:
- name: web
image: kiamol/ch03-numbers-web:v2
env:
- name: http_proxy
value: http://localhost:1080
- name: RngApi__Url
value: http://localhost/api
- name: proxy
image: kiamol/ch07-simple-proxy
env:
- name: Proxy__Port
value: "1080"
- name: Proxy__Request__UriMap__Source
value: http://localhost/api
- name: Proxy__Request__UriMap__Target
value: http://numbers-api/sixeyed/kiamol/master/ch03/numbers/rng
volumes:
- name: config-dir
emptyDir: {}

View File

@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: numbers-web
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: numbers-web
template:
metadata:
labels:
app: numbers-web
spec:
containers:
- name: web
image: kiamol/ch03-numbers-web
env:
- name: http_proxy
value: http://localhost:1080
- name: RngApi__Url
value: http://localhost/api
- name: proxy
image: kiamol/ch07-simple-proxy
env:
- name: Proxy__Port
value: "1080"
- name: Proxy__Request__UriMap__Source
value: http://localhost/api
- name: Proxy__Request__UriMap__Target
value: http://numbers-api/sixeyed/kiamol/master/ch03/numbers/rng

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: numbers-web
labels:
kiamol: ch07
spec:
ports:
- port: 8090
targetPort: 80
selector:
app: numbers-web
type: LoadBalancer

View File

@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: numbers-web
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: numbers-web
template:
metadata:
labels:
app: numbers-web
spec:
containers:
- name: web
image: kiamol/ch03-numbers-web

View File

@@ -0,0 +1,7 @@
# The Service Hotel
Presentation from [Containers Today](https://www.containerstoday.com/sweden/wp-content/uploads/sites/12/2019/12/How-to-Design-a-Container-platform-Sune-Keller-AlmBrand.pdf).
Copied here with kind permission from the author, Docker Captain [Sune Keller](https://twitter.com/sirlatrom).
Thanks Sune :)

View File

@@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: data
mountPath: /data-rw
- name: file-reader
image: kiamol/ch03-sleep
volumeMounts:
- name: data
mountPath: /data-ro
readOnly: true
volumes:
- name: data
emptyDir: {}

View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
initContainers:
- name: init-html
image: kiamol/ch03-sleep
command: ['sh', '-c', "echo '<!DOCTYPE html><html><body><h1>KIAMOL Ch07</h1></body></html>' > /data/index.html"]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: sleep
image: kiamol/ch03-sleep
- name: server
image: kiamol/ch03-sleep
command: ['sh', '-c', 'while true; do echo -e "HTTP/1.1 200 OK\nContent-Type: text/html\nContent-Length: 62\n\n$(cat /data-ro/index.html)" | nc -l -p 8080; done']
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: data
mountPath: /data-ro
readOnly: true
volumes:
- name: data
emptyDir: {}

View File

@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
version: shared
spec:
shareProcessNamespace: true
containers:
- name: sleep
image: kiamol/ch03-sleep
- name: server
image: kiamol/ch03-sleep
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 7\n\nkiamol' | nc -l -p 8080; done"]
ports:
- containerPort: 8080
name: http

View File

@@ -0,0 +1,24 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
- name: server
image: kiamol/ch03-sleep
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 7\n\nkiamol' | nc -l -p 8080; done"]
ports:
- containerPort: 8080
name: http

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: timecheck-config
data:
appsettings.json: |-
{
"Application": {
"Version": "1.1",
"Environment": "TEST"
},
"Timer": {
"IntervalSeconds": "7"
}
}

View File

@@ -0,0 +1,79 @@
apiVersion: v1
kind: Service
metadata:
name: timecheck
labels:
kiamol: ch07
spec:
ports:
- port: 8080
targetPort: 8080
name: healthz
- port: 8081
targetPort: 8081
name: metrics
selector:
app: timecheck
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: timecheck
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: timecheck
template:
metadata:
labels:
app: timecheck
version: v4
spec:
initContainers:
- name: init-config
image: kiamol/ch03-sleep
command: ['sh', '-c', 'cp /config-in/appsettings.json /config-out/appsettings.json']
volumeMounts:
- name: config-map
mountPath: /config-in
- name: config-dir
mountPath: /config-out
containers:
- name: timecheck
image: kiamol/ch07-timecheck
volumeMounts:
- name: config-dir
mountPath: /config
readOnly: true
- name: logs-dir
mountPath: /logs
- name: logger
image: kiamol/ch03-sleep
command: ['sh', '-c', 'tail -f /logs-ro/timecheck.log']
volumeMounts:
- name: logs-dir
mountPath: /logs-ro
readOnly: true
- name: healthz
image: kiamol/ch03-sleep
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 17\n\n{\"status\": \"OK\"}' | nc -l -p 8080; done"]
ports:
- containerPort: 8080
name: http
- name: metrics
image: kiamol/ch03-sleep
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 104\n\n# HELP timechecks_total The total number timechecks.\n# TYPE timechecks_total counter\ntimechecks_total 6' | nc -l -p 8081; done"]
ports:
- containerPort: 8081
name: http
volumes:
- name: config-map
configMap:
name: timecheck-config
- name: config-dir
emptyDir: {}
- name: logs-dir
emptyDir: {}

View File

@@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: timecheck
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: timecheck
template:
metadata:
labels:
app: timecheck
version: v2
spec:
initContainers:
- name: init-config
image: kiamol/ch03-sleep
command: ['sh', '-c', "cat /config-in/appsettings.json | jq --arg APP_ENV \"$APP_ENVIRONMENT\" '.Application.Environment=$APP_ENV' > /config-out/appsettings.json"]
env:
- name: APP_ENVIRONMENT
value: TEST
volumeMounts:
- name: config-map
mountPath: /config-in
- name: config-dir
mountPath: /config-out
containers:
- name: timecheck
image: kiamol/ch07-timecheck
volumeMounts:
- name: config-dir
mountPath: /config
readOnly: true
volumes:
- name: config-map
configMap:
name: timecheck-config
- name: config-dir
emptyDir: {}

View File

@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: timecheck
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: timecheck
template:
metadata:
labels:
app: timecheck
version: v3
spec:
initContainers:
- name: init-config
image: kiamol/ch03-sleep
command: ['sh', '-c', 'cp /config-in/appsettings.json /config-out/appsettings.json']
volumeMounts:
- name: config-map
mountPath: /config-in
- name: config-dir
mountPath: /config-out
containers:
- name: timecheck
image: kiamol/ch07-timecheck
volumeMounts:
- name: config-dir
mountPath: /config
readOnly: true
- name: logs-dir
mountPath: /logs
- name: logger
image: kiamol/ch03-sleep
command: ['sh', '-c', 'tail -f /logs-ro/timecheck.log']
volumeMounts:
- name: logs-dir
mountPath: /logs-ro
readOnly: true
volumes:
- name: config-map
configMap:
name: timecheck-config
- name: config-dir
emptyDir: {}
- name: logs-dir
emptyDir: {}

View File

@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: timecheck
labels:
kiamol: ch07
spec:
selector:
matchLabels:
app: timecheck
template:
metadata:
labels:
app: timecheck
spec:
containers:
- name: timecheck
image: kiamol/ch07-timecheck