Merge remote-tracking branch 'origin/main' into gcp_secret_manager_adapter

This commit is contained in:
André Laszlo
2024-11-27 13:42:21 +01:00
27 changed files with 675 additions and 19 deletions

View File

@@ -7,6 +7,18 @@ class CliSecretsTest < CliTestCase
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
end
test "fetch missing --acount" do
assert_equal \
"No value provided for required options '--account'",
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
end
test "fetch without required --account" do
assert_equal \
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
run_command("fetch", "foo", "bar", "baz", "--adapter", "test_optional_account")
end
test "extract" do
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
end

View File

@@ -39,7 +39,10 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
"busybox" => {
"service" => "custom-busybox",
"image" => "busybox:latest",
"host" => "1.1.1.7"
"host" => "1.1.1.7",
"proxy" => {
"host" => "busybox.example.com"
}
}
}
}
@@ -166,6 +169,18 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
new_command(:mysql).remove_image.join(" ")
end
test "deploy" do
assert_equal \
"docker exec kamal-proxy kamal-proxy deploy custom-busybox --target=\"172.1.0.2:80\" --host=\"busybox.example.com\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
new_command(:busybox).deploy(target: "172.1.0.2").join(" ")
end
test "remove" do
assert_equal \
"docker exec kamal-proxy kamal-proxy remove custom-busybox",
new_command(:busybox).remove.join(" ")
end
private
def new_command(accessory)
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)

View File

@@ -158,6 +158,20 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder.push.join(" ")
end
test "push with sbom" do
builder = new_builder_command(builder: { "sbom" => true })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom true .",
builder.push.join(" ")
end
test "push with sbom false" do
builder = new_builder_command(builder: { "sbom" => false })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom false .",
builder.push.join(" ")
end
test "mirror count" do
command = new_builder_command
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")

View File

@@ -63,6 +63,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
"options" => {
"cpus" => "4",
"memory" => "2GB"
},
"proxy" => {
"host" => "monitoring.example.com"
}
}
}
@@ -161,4 +164,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
@deploy[:accessories]["mysql"]["network"] = "database"
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
end
test "proxy" do
assert @config.accessory(:monitoring).running_proxy?
assert_equal [ "monitoring.example.com" ], @config.accessory(:monitoring).proxy.hosts
end
end

View File

@@ -144,6 +144,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
assert_equal "mode=max", config.builder.provenance
end
test "sbom" do
assert_nil config.builder.sbom
end
test "setting sbom" do
@deploy[:builder]["sbom"] = true
assert_equal true, config.builder.sbom
end
test "local disabled but no remote set" do
@deploy[:builder]["local"] = false

View File

@@ -20,6 +20,7 @@ COPY *.sh .
COPY app/ app/
COPY app_with_roles/ app_with_roles/
COPY app_with_traefik/ app_with_traefik/
COPY app_with_proxied_accessory/ app_with_proxied_accessory/
RUN rm -rf /root/.ssh
RUN ln -s /shared/ssh /root/.ssh
@@ -30,6 +31,7 @@ RUN git config --global user.name "Deployer"
RUN cd app && git init && git add . && git commit -am "Initial version"
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
RUN cd app_with_proxied_accessory && git init && git add . && git commit -am "Initial version"
HEALTHCHECK --interval=1s CMD pgrep sleep

View File

@@ -0,0 +1,9 @@
FROM registry:4443/nginx:1-alpine-slim
COPY default.conf /etc/nginx/conf.d/default.conf
ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -0,0 +1,44 @@
service: app_with_proxied_accessory
image: app_with_proxied_accessory
servers:
- vm1
env:
clear:
CLEAR_TOKEN: 4321
CLEAR_TAG: ""
HOST_TOKEN: "${HOST_TOKEN}"
asset_path: /usr/share/nginx/html/versions
proxy:
host: 127.0.0.1
registry:
server: registry:4443
username: root
password: root
builder:
driver: docker
arch: <%= Kamal::Utils.docker_arch %>
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
accessories:
busybox:
service: custom-busybox
image: registry:4443/busybox:1.36.0
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles:
- web
netcat:
service: netcat
image: registry:4443/busybox:1.36.0
cmd: >
sh -c 'echo "Starting netcat..."; while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello Ruby" | nc -l -p 80; done'
roles:
- web
port: 12345:80
proxy:
host: netcat
ssl: false
healthcheck:
interval: 1
timeout: 1
path: "/"

View File

@@ -0,0 +1,17 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -0,0 +1,63 @@
require_relative "integration_test"
class ProxiedAccessoryTest < IntegrationTest
test "boot, stop, start, restart, logs, remove" do
@app = "app_with_proxied_accessory"
kamal :deploy
kamal :accessory, :boot, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :stop, :netcat
assert_accessory_not_running :netcat
assert_netcat_not_found
kamal :accessory, :start, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :restart, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :remove, :netcat, "-y"
assert_accessory_not_running :netcat
assert_netcat_not_found
end
private
def assert_accessory_running(name)
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
end
def assert_accessory_not_running(name)
assert_no_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
end
def accessory_details(name)
kamal :accessory, :details, name, capture: true
end
def assert_netcat_is_up
response = netcat_response
debug_response_code(response, "200")
assert_equal "200", response.code
end
def assert_netcat_not_found
response = netcat_response
debug_response_code(response, "404")
assert_equal "404", response.code
end
def netcat_response
uri = URI.parse("http://127.0.0.1:12345/up")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri)
request["Host"] = "netcat"
http.request(request)
end
end

View File

@@ -0,0 +1,98 @@
require "test_helper"
class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
test "fetch" do
stub_ticks.with("aws --version 2> /dev/null")
stub_ticks
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default")
.returns(<<~JSON)
{
"SecretValues": [
{
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
"Name": "secret",
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
"SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-01-01T00:00:00.000000"
},
{
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
"Name": "secret2",
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
"SecretString": "{\\"KEY3\\":\\"VALUE3\\"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-01-01T00:00:00.000000"
}
],
"Errors": []
}
JSON
json = JSON.parse(shellunescape(run_command("fetch", "secret/KEY1", "secret/KEY2", "secret2/KEY3")))
expected_json = {
"secret/KEY1"=>"VALUE1",
"secret/KEY2"=>"VALUE2",
"secret2/KEY3"=>"VALUE3"
}
assert_equal expected_json, json
end
test "fetch with secret names" do
stub_ticks.with("aws --version 2> /dev/null")
stub_ticks
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 --profile default")
.returns(<<~JSON)
{
"SecretValues": [
{
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
"Name": "secret",
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
"SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-01-01T00:00:00.000000"
}
],
"Errors": []
}
JSON
json = JSON.parse(shellunescape(run_command("fetch", "--from", "secret", "KEY1", "KEY2")))
expected_json = {
"secret/KEY1"=>"VALUE1",
"secret/KEY2"=>"VALUE2"
}
assert_equal expected_json, json
end
test "fetch without CLI installed" do
stub_ticks_with("aws --version 2> /dev/null", succeed: false)
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
end
assert_equal "AWS CLI is not installed", error.message
end
private
def run_command(*command)
stdouted do
Kamal::Cli::Secrets.start \
[ *command,
"-c", "test/fixtures/deploy_with_accessories.yml",
"--adapter", "aws_secrets_manager",
"--account", "default" ]
end
end
end

View File

@@ -0,0 +1,186 @@
require "test_helper"
class DopplerAdapterTest < SecretAdapterTestCase
setup do
`true` # Ensure $? is 0
end
test "fetch" do
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
stub_ticks.with("doppler me --json 2> /dev/null")
stub_ticks
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json -p my-project -c prd")
.returns(<<~JSON)
{
"SECRET1": {
"computed":"secret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET1": {
"computed":"fsecret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET2": {
"computed":"fsecret2",
"computedVisibility":"unmasked",
"note":""
}
}
JSON
json = JSON.parse(
shellunescape run_command("fetch", "--from", "my-project/prd", "SECRET1", "FSECRET1", "FSECRET2")
)
expected_json = {
"SECRET1"=>"secret1",
"FSECRET1"=>"fsecret1",
"FSECRET2"=>"fsecret2"
}
assert_equal expected_json, json
end
test "fetch having DOPPLER_TOKEN" do
ENV["DOPPLER_TOKEN"] = "dp.st.xxxxxxxxxxxxxxxxxxxxxx"
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
stub_ticks.with("doppler me --json 2> /dev/null")
stub_ticks
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json ")
.returns(<<~JSON)
{
"SECRET1": {
"computed":"secret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET1": {
"computed":"fsecret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET2": {
"computed":"fsecret2",
"computedVisibility":"unmasked",
"note":""
}
}
JSON
json = JSON.parse(
shellunescape run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2")
)
expected_json = {
"SECRET1"=>"secret1",
"FSECRET1"=>"fsecret1",
"FSECRET2"=>"fsecret2"
}
assert_equal expected_json, json
ENV.delete("DOPPLER_TOKEN")
end
test "fetch with folder in secret" do
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
stub_ticks.with("doppler me --json 2> /dev/null")
stub_ticks
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json -p my-project -c prd")
.returns(<<~JSON)
{
"SECRET1": {
"computed":"secret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET1": {
"computed":"fsecret1",
"computedVisibility":"unmasked",
"note":""
},
"FSECRET2": {
"computed":"fsecret2",
"computedVisibility":"unmasked",
"note":""
}
}
JSON
json = JSON.parse(
shellunescape run_command("fetch", "my-project/prd/SECRET1", "my-project/prd/FSECRET1", "my-project/prd/FSECRET2")
)
expected_json = {
"SECRET1"=>"secret1",
"FSECRET1"=>"fsecret1",
"FSECRET2"=>"fsecret2"
}
assert_equal expected_json, json
end
test "fetch without --from" do
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
stub_ticks.with("doppler me --json 2> /dev/null")
error = assert_raises RuntimeError do
run_command("fetch", "FSECRET1", "FSECRET2")
end
assert_equal "Missing project or config from '--from=project/config' option", error.message
end
test "fetch with signin" do
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
stub_ticks_with("doppler me --json 2> /dev/null", succeed: false)
stub_ticks_with("doppler login -y", succeed: true).returns("")
stub_ticks.with("doppler secrets get SECRET1 --json -p my-project -c prd").returns(single_item_json)
json = JSON.parse(shellunescape(run_command("fetch", "--from", "my-project/prd", "SECRET1")))
expected_json = {
"SECRET1"=>"secret1"
}
assert_equal expected_json, json
end
test "fetch without CLI installed" do
stub_ticks_with("doppler --version 2> /dev/null", succeed: false)
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT")))
end
assert_equal "Doppler CLI is not installed", error.message
end
private
def run_command(*command)
stdouted do
Kamal::Cli::Secrets.start \
[ *command,
"-c", "test/fixtures/deploy_with_accessories.yml",
"--adapter", "doppler" ]
end
end
def single_item_json
<<~JSON
{
"SECRET1": {
"computed":"secret1",
"computedVisibility":"unmasked",
"note":""
}
}
JSON
end
end

View File

@@ -2,6 +2,7 @@ require "bundler/setup"
require "active_support/test_case"
require "active_support/testing/autorun"
require "active_support/testing/stream"
require "rails/test_unit/line_filtering"
require "debug"
require "mocha/minitest" # using #stubs that can alter returns
require "minitest/autorun" # using #stub that take args
@@ -32,6 +33,7 @@ end
class ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
extend Rails::LineFiltering
private
def stdouted