Skip to content

Commit 8722b27

Browse files
authored
Merge pull request #1 from caseycs/add-docker-build
Add docker build
2 parents 6961820 + 8d20361 commit 8722b27

File tree

9 files changed

+183
-39
lines changed

9 files changed

+183
-39
lines changed

.github/workflows/release.yaml

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,88 @@ name: Release
33
on:
44
push:
55
tags:
6-
- 'v*' # Trigger only when a tag starting with 'v' is pushed (e.g., v1.0.0)
6+
- "v*" # Trigger only when a tag starting with 'v' is pushed (e.g., v1.0.0)
77

88
permissions:
99
contents: write
1010

11+
env:
12+
ALPINE_GIT_VERSION: v2.47.2
13+
1114
jobs:
1215
build:
13-
name: Build Go Binaries
16+
name: Publish binaries
1417
runs-on: ubuntu-20.04
1518

1619
steps:
17-
- name: Get release version from tag
18-
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
19-
2020
- name: Checkout code
2121
uses: actions/checkout@v4
2222

2323
- name: Set up Go
2424
uses: actions/setup-go@v5
2525
with:
26-
go-version: '1.21' # Change to the required Go version
26+
go-version: "1.21" # Change to the required Go version
2727

2828
- name: Install dependencies
2929
run: go mod tidy
3030

3131
- name: Build binaries for multiple platforms
3232
run: |
3333
mkdir -p dist
34-
GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64 .
35-
GOOS=linux GOARCH=arm64 go build -o dist/app-linux-arm64 .
36-
GOOS=darwin GOARCH=arm64 go build -o dist/app-macos-arm64 .
34+
GOOS=linux GOARCH=amd64 go build -o dist/maskcmd-linux-amd64 .
35+
GOOS=linux GOARCH=arm64 go build -o dist/maskcmd-linux-arm64 .
36+
GOOS=darwin GOARCH=arm64 go build -o dist/maskcmd-macos-arm64 .
3737
3838
- name: Create GitHub Release
3939
id: create_release
4040
uses: softprops/action-gh-release@v2
4141
with:
42-
tag_name: ${{ env.RELEASE_VERSION }}
43-
name: Release ${{ env.RELEASE_VERSION }}
44-
body: "Automated release for version ${{ env.RELEASE_VERSION }}"
42+
tag_name: ${{ github.ref_name }}
43+
name: Release ${{ github.ref_name }}
44+
body: "Automated release for version ${{ github.ref_name }}"
4545
draft: false
4646
prerelease: false
4747
files: |
48-
dist/app-linux-amd64
49-
dist/app-linux-arm64
50-
dist/app-macos-arm64
48+
dist/maskcmd-linux-amd64
49+
dist/maskcmd-linux-arm64
50+
dist/maskcmd-macos-arm64
5151
env:
52-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53+
54+
docker:
55+
name: Publish docker image
56+
runs-on: ubuntu-20.04
57+
needs: build
58+
steps:
59+
- name: Checkout
60+
uses: actions/checkout@v4
61+
62+
- name: Set up Docker Buildx
63+
uses: docker/setup-buildx-action@v3
64+
65+
- name: Log in to Docker Hub
66+
uses: docker/login-action@v3
67+
with:
68+
username: ${{ secrets.DOCKERHUB_USERNAME }}
69+
password: ${{ secrets.DOCKERHUB_PAT }}
70+
71+
- name: Extract metadata (tags, labels)
72+
id: meta
73+
uses: docker/metadata-action@v5
74+
with:
75+
images: caseycs/maskcmd
76+
tags: |
77+
type=semver,pattern=${{ env.ALPINE_GIT_VERSION }}-{{major}}{{minor}}{{patch}}
78+
79+
- name: Build and Push Docker image
80+
uses: docker/build-push-action@v6
81+
with:
82+
context: .
83+
platforms: |
84+
linux/amd64
85+
linux/arm64
86+
build-args: |
87+
ALPINE_GIT_VERSION=${{ env.ALPINE_GIT_VERSION }}
88+
MASKCMD_VERSION=${{ github.ref_name }}
89+
push: true
90+
tags: ${{ steps.meta.outputs.tags }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.idea
1+
.idea
2+
maskcmd

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ARG ALPINE_GIT_VERSION=latest
2+
FROM alpine/git:${ALPINE_GIT_VERSION}
3+
4+
ARG TARGETOS
5+
ARG TARGETARCH
6+
7+
ARG MASKCMD_VERSION=v0.0.8
8+
9+
RUN wget -O /usr/local/bin/maskcmd https://github.com/caseycs/maskcmd/releases/download/$MASKCMD_VERSION/maskcmd-$TARGETOS-$TARGETARCH \
10+
&& chmod +x /usr/local/bin/maskcmd

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ Useful for bash scripts within K8S native pipelines, like Argo Workflows.
55

66
## Usage examples
77

8+
### Argo Workflow
9+
10+
```bash
11+
kubectl apply -f argo-workflow-example/secret.yaml
12+
argo submit argo-workflow-example/example.yaml -w --log
13+
Name: maskcmd-example-mm2nj
14+
Namespace: default
15+
ServiceAccount: unset (will run with the default ServiceAccount)
16+
Status: Pending
17+
Created: Sun Mar 16 22:42:25 +0100 (now)
18+
Progress:
19+
maskcmd-example-mm2nj: + git clone https://x-token-auth:*****@bitbucket.org/project1/repo1.git
20+
maskcmd-example-mm2nj: Cloning into 'repo1'...
21+
maskcmd-example-mm2nj: remote: You may not have access to this repository or it no longer exists in this workspace. If you think this repository exists and you have access, make sure you are authenticated.
22+
maskcmd-example-mm2nj: fatal: repository 'https://bitbucket.org/project1/repo1.git/' not found
23+
maskcmd-example-mm2nj: Error: child command returned exit code: 128
24+
maskcmd-example-mm2nj: time="2025-03-16T21:42:28.457Z" level=info msg="sub-process exited" argo=true error="<nil>"
25+
maskcmd-example-mm2nj: Error: exit status 128
26+
maskcmd-example-mm2nj Failed at 2025-03-16 22:42:35 +0100 CET
27+
```
28+
29+
Notice that there no `bitbucket-repo1-token` (secret value) in the output, but just asterisks (`*****@`).
30+
31+
[Example K8S manigests](/argo-workflow-example).
32+
833
### Shell scripts
934

1035
Mask files content in certain dir:
@@ -46,3 +71,7 @@ Error: child command returned exit code: 5
4671
echo $?
4772
5
4873
```
74+
75+
## Docker image
76+
77+
[Dockerfile](/Dockerfile) is based on recent [alpine/git](https://hub.docker.com/r/alpine/git): https://hub.docker.com/r/caseycs/maskcmd/tags

argo-workflow-example/example.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: argoproj.io/v1alpha1
2+
kind: Workflow
3+
metadata:
4+
generateName: maskcmd-example-
5+
spec:
6+
entrypoint: demo
7+
templates:
8+
- name: demo
9+
container:
10+
image: caseycs/maskcmd:v2.47.2-008
11+
command: [maskcmd, --secrets-dir, /secret/, --, sh, -exc]
12+
args:
13+
- |
14+
git clone https://x-token-auth:$(cat /secret/bitbucket-repo1/token)@bitbucket.org/project1/repo1.git
15+
volumeMounts:
16+
- name: secret-bitbucket-repo1
17+
mountPath: "/secret/bitbucket-repo1"
18+
volumes:
19+
- name: secret-bitbucket-repo1
20+
secret:
21+
secretName: bitbucket-repo1

argo-workflow-example/secret.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: bitbucket-repo1
5+
type: Opaque
6+
data:
7+
token: bitbucket-repo1-token # bitbucket-repo1-token

command.go

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"errors"
66
"fmt"
7-
"github.com/spf13/cobra"
87
"io"
98
"os"
109
"os/exec"
@@ -13,6 +12,8 @@ import (
1312
"sort"
1413
"strings"
1514
"sync"
15+
16+
"github.com/spf13/cobra"
1617
)
1718

1819
const maxSecretLength = 4096
@@ -36,7 +37,7 @@ func cmdMask(cmd *cobra.Command, args []string) error {
3637
if secretsDir, _ := cmd.Flags().GetString("secrets-dir"); secretsDir != "" {
3738
secretsFromFiles, err := readSecretsFromDir(secretsDir)
3839
if err != nil {
39-
return fmt.Errorf("error reading secrets from directory")
40+
return fmt.Errorf("error reading secrets from directory: %v", err)
4041
}
4142
masks = append(masks, secretsFromFiles...)
4243
}
@@ -185,29 +186,25 @@ func readSecretsFromDir(dirPath string) ([]string, error) {
185186
return err
186187
}
187188

188-
if !info.IsDir() { // Read only files
189-
if info.Size() > maxSecretLength {
190-
return fmt.Errorf("secret file %s is too large (above %dkb)", path, maxSecretLength/1024)
191-
}
189+
// Resolve symlinks
190+
resolvedInfo, err := os.Lstat(path)
191+
if err != nil {
192+
return err // skip broken symlinks or inaccessible files
193+
}
192194

193-
file, err := os.Open(path)
194-
if err != nil {
195-
return err
196-
}
197-
defer file.Close()
195+
if !resolvedInfo.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
196+
return nil
197+
}
198198

199-
scanner := bufio.NewScanner(file)
200-
for scanner.Scan() {
201-
secret := strings.TrimSpace(scanner.Text())
202-
if secret != "" {
203-
secrets = append(secrets, secret)
204-
}
205-
}
199+
if info.Size() > maxSecretLength {
200+
return fmt.Errorf("secret file %s is too large (above %dkb)", path, maxSecretLength/1024)
201+
}
206202

207-
if err := scanner.Err(); err != nil {
208-
return err
209-
}
203+
secret, err := os.ReadFile(path)
204+
if err != nil {
205+
return err
210206
}
207+
secrets = append(secrets, strings.TrimSpace(string(secret)))
211208
return nil
212209
})
213210

command_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package main
22

33
import (
44
"bytes"
5-
"github.com/spf13/cobra"
65
"os"
76
"path/filepath"
87
"strings"
98
"testing"
9+
10+
"github.com/spf13/cobra"
1011
)
1112

1213
func TestMaskLine(t *testing.T) {
@@ -149,6 +150,46 @@ func TestCmdMask_MaskSecretsFromDirectory(t *testing.T) {
149150
}
150151
}
151152

153+
func TestCmdMask_MaskSecretsFromDirectory_Symlinks(t *testing.T) {
154+
// Create temporary directories and symlinks similar to mounted K8S pod secret
155+
testTempDir, err := os.MkdirTemp("", "secrets_test")
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
defer os.RemoveAll(testTempDir) // Cleanup after test
160+
161+
if err := os.Mkdir(testTempDir+"/..random-name", 0755); err != nil {
162+
t.Fatal(err)
163+
}
164+
defer os.RemoveAll(testTempDir + "/..random-name") // Cleanup after test
165+
166+
// Create secret file
167+
createTempSecretFile(testTempDir+"/..random-name", "token", "mypassword")
168+
if err = os.Symlink(testTempDir+"/..random-name/token", testTempDir+"/token"); err != nil {
169+
t.Fatal(err)
170+
}
171+
172+
// Symlink like mounted K8S secret
173+
if err = os.Symlink(testTempDir+"/..random-name", testTempDir+"/..data"); err != nil {
174+
t.Fatal(err)
175+
}
176+
177+
stdout, stderr, err := executeCommand(buildCmdMask(), "--secrets-dir", testTempDir, "--", "echo", "Password is mypassword")
178+
if err != nil {
179+
t.Fatalf("Error executing command: %v", err)
180+
}
181+
182+
expectedStdOut := "Password is *****"
183+
if strings.TrimSpace(stdout) != expectedStdOut {
184+
t.Errorf("Expected stdout %q, got %q", expectedStdOut, stdout)
185+
}
186+
187+
expectedStdErr := ""
188+
if strings.TrimSpace(stderr) != expectedStdErr {
189+
t.Errorf("Expected stderr %q, got %q", expectedStdErr, stderr)
190+
}
191+
}
192+
152193
func TestCmdMask_CustomExitCode(t *testing.T) {
153194
os.Setenv("SECRET", "mysecret")
154195
defer os.Unsetenv("SECRET")

maskcmd

-3.47 MB
Binary file not shown.

0 commit comments

Comments
 (0)