Author SHA1 Message Date
jmug 1892fd9e8a feat: arr and map methods now output json to make the read, mutate, write loop easier.
release-clis / release (push) Successful in 43s
build-secretsserver / build (push) Successful in 59s
2026-10-30 13:50:55 -07:00
jmug aa3d497139 feat: Support setting the value of array/map secrets from a json file.
build-secretsserver / build (push) Successful in 39s
2026-10-30 13:32:35 -07:00
jmug 03247415b2 feat: Support configuration file for hmsecrets.
build-secretsserver / build (push) Successful in 1m39s
release-clis / release (push) Successful in 2m25s
2026-10-29 14:35:46 -07:00
jmug 3b29c2cb1d chore: Set up goreleaser to build and release binaries.
build-secretsserver / build (push) Successful in 1m23s
release-clis / release (push) Successful in 2m10s
2026-10-29 13:01:15 -07:00
jmugandjmug 36af437794 ci: Build secrets server on PRs, publish on releases. (#1)
build-secretsserver / build (push) Successful in 45s
Co-authored-by: jmug <u.g.a.mariano@gmail.com>
Reviewed-on: #1
2026-10-25 03:10:37 -07:00
17 changed files with 287 additions and 54 deletions

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
name: build-secretsserver
on:
push:
branches:
- "main"
# Only run on release tags.
tags:
- "v*"
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# Unfortunately gotta use a PAT,
# workflow tokens can't access the registry yet.
- name: Log in to the registry
if: ${{ gitea.ref_type == 'tag' }}
uses: docker/login-action@v3
with:
registry: git.handmadecities.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Compute tags
id: meta
uses: docker/metadata-action@v5
with:
images: git.handmadecities.com/${{ gitea.repository }}/secretsserver
tags: |
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v6
with:
file: docker/secretsserver/Dockerfile
context: .
# Only push on releases.
push: ${{ gitea.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
+26
View File
@@ -0,0 +1,26 @@
name: release-clis
on:
push:
tags:
- "v*"
permissions:
code: read
releases: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0
with:
args: release --clean
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+2
View File
@@ -118,3 +118,5 @@ tags
# Persistent undo # Persistent undo
[._]*.un~ [._]*.un~
# Added by goreleaser init:
dist/
+47
View File
@@ -0,0 +1,47 @@
version: 2
gitea_urls:
api: https://git.handmadecities.com/api/v1
download: https://git.handmadecities.com/
before:
hooks:
- go mod tidy
builds:
- id: hmpki
main: ./cmd/hmpki
binary: hmpki
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- arm64
- amd64
- id: hmsecrets
main: ./cmd/hmsecrets
binary: hmsecrets
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- arm64
- amd64
archives:
- formats: [tar.gz]
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
formats: [zip]
+3 -1
View File
@@ -1,6 +1,8 @@
package main package main
import "git.handmadecities.com/HMC/shhh/internal/clis/hmsecrets" import (
"git.handmadecities.com/HMC/shhh/internal/clis/hmsecrets"
)
func main() { func main() {
hmsecrets.Execute() hmsecrets.Execute()
+1
View File
@@ -11,4 +11,5 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.45.0 // indirect golang.org/x/sys v0.45.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+2 -2
View File
@@ -2,8 +2,8 @@ package hmsecrets
import "git.handmadecities.com/HMC/shhh/pkg/secretsclient" import "git.handmadecities.com/HMC/shhh/pkg/secretsclient"
func getClient(base, ca, cert, certKey string) secretsclient.Client { func getClient() secretsclient.Client {
cli, err := secretsclient.New(base, ca, cert, certKey) cli, err := secretsclient.New(conf.APIBase, conf.CA, conf.ClientCert, conf.ClientKey)
if err != nil { if err != nil {
panic(err) panic(err)
} }
+83
View File
@@ -0,0 +1,83 @@
package hmsecrets
import (
"errors"
"fmt"
"os"
"path"
"runtime"
"gopkg.in/yaml.v3"
)
type config struct {
APIBase string `yaml:"api-base"`
CA string `yaml:"ca"`
ClientCert string `yaml:"client-cert"`
ClientKey string `yaml:"client-key"`
}
func mustLoadConfig() config {
var baseConfigDir string
var err error
switch runtime.GOOS {
case "windows":
fallthrough
case "linux":
baseConfigDir, err = os.UserConfigDir()
if err != nil {
panic(fmt.Errorf("determining user config dir: %w", err))
}
case "darwin":
// We want $HOME/.config in macOS.
homeDir, err := os.UserHomeDir()
if err != nil {
panic(fmt.Errorf("determining user home dir: %w", err))
}
baseConfigDir = path.Join(homeDir, ".config")
}
configDir := path.Join(baseConfigDir, "hmsecrets")
configBytes, err := os.ReadFile(path.Join(configDir, "config.yaml"))
if err != nil {
panic(fmt.Errorf("reading config file: %w", err))
}
var conf config
err = yaml.Unmarshal(configBytes, &conf)
if err != nil {
panic(fmt.Errorf("parsing config file: %w", err))
}
if conf.APIBase == "" {
panic(errors.New("api base must be present in config"))
}
if conf.CA == "" {
conf.CA = path.Join(configDir, "ca.crt")
}
if !fileAccesible(conf.CA) {
panic(fmt.Errorf("ca file '%s' does not exist or is inaccesible", conf.CA))
}
if conf.ClientCert == "" {
conf.ClientCert = path.Join(configDir, "client.crt")
}
if !fileAccesible(conf.ClientCert) {
panic(fmt.Errorf("client cert file '%s' does not exist or is inaccesible", conf.ClientCert))
}
if conf.ClientKey == "" {
conf.ClientKey = path.Join(configDir, "client.key")
}
if !fileAccesible(conf.ClientKey) {
panic(fmt.Errorf("client key file '%s' does not exist or is inaccesible", conf.ClientKey))
}
return conf
}
func fileAccesible(f string) bool {
_, err := os.Stat(f)
return err == nil
}
+1 -1
View File
@@ -13,7 +13,7 @@ var delCmd = &cobra.Command{
Short: "del [key]", Short: "del [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
err := cli.Delete(args[0]) err := cli.Delete(args[0])
if err != nil { if err != nil {
return err return err
+7 -2
View File
@@ -1,6 +1,7 @@
package hmsecrets package hmsecrets
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -15,12 +16,16 @@ var getArrCmd = &cobra.Command{
Short: "get-array [key]", Short: "get-array [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
secret, err := cli.GetStringArr(args[0]) secret, err := cli.GetStringArr(args[0])
if err != nil { if err != nil {
return err return err
} }
fmt.Println(secret) secretJSON, err := json.MarshalIndent(secret, "", " ")
if err != nil {
return err
}
fmt.Println(string(secretJSON))
return nil return nil
}, },
} }
+7 -2
View File
@@ -1,6 +1,7 @@
package hmsecrets package hmsecrets
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -15,12 +16,16 @@ var getMapCmd = &cobra.Command{
Short: "get-map [key]", Short: "get-map [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
secret, err := cli.GetStringMap(args[0]) secret, err := cli.GetStringMap(args[0])
if err != nil { if err != nil {
return err return err
} }
fmt.Println(secret) secretJSON, err := json.MarshalIndent(secret, "", " ")
if err != nil {
return err
}
fmt.Println(string(secretJSON))
return nil return nil
}, },
} }
+1 -1
View File
@@ -15,7 +15,7 @@ var getCmd = &cobra.Command{
Short: "get [key]", Short: "get [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
secret, err := cli.GetString(args[0]) secret, err := cli.GetString(args[0])
if err != nil { if err != nil {
return err return err
+10 -38
View File
@@ -7,44 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var apiBase string var conf config
var caFile string
var clientCert string
var clientKey string
func init() {
rootCmd.PersistentFlags().StringVar(
&apiBase,
"api-base",
"",
"base url for the service",
)
rootCmd.MarkPersistentFlagRequired("api-base")
rootCmd.PersistentFlags().StringVar(
&caFile,
"ca-file",
"",
"CA cert file",
)
rootCmd.MarkPersistentFlagRequired("ca-file")
rootCmd.PersistentFlags().StringVar(
&clientCert,
"client-cert",
"",
"certificate to be used for client mTLS auth",
)
rootCmd.MarkPersistentFlagRequired("client-cert")
rootCmd.PersistentFlags().StringVar(
&clientKey,
"client-key",
"",
"private key for the certificate to be used for client mTLS auth",
)
rootCmd.MarkPersistentFlagRequired("client-key")
}
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "hmsecrets", Use: "hmsecrets",
@@ -52,6 +15,15 @@ var rootCmd = &cobra.Command{
} }
func Execute() { func Execute() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("%+v\n", r)
os.Exit(1)
}
}()
conf = mustLoadConfig()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
+26 -3
View File
@@ -1,22 +1,35 @@
package hmsecrets package hmsecrets
import ( import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var arrValue []string var arrValue []string
var arrFile string
func init() { func init() {
rootCmd.AddCommand(setArrCmd) rootCmd.AddCommand(setArrCmd)
setArrCmd.Flags().StringArrayVarP( setArrCmd.Flags().StringArrayVarP(
&arrValue, &arrValue,
"array-value", "value",
"v", "v",
nil, nil,
"array value to store in the secret", "array value to store in the secret",
) )
setArrCmd.MarkFlagRequired("array-value") setArrCmd.Flags().StringVarP(
&arrFile,
"value-file",
"f",
"",
"file that contains the secret value (in json) to set",
)
setArrCmd.MarkFlagsOneRequired("value-file", "value")
} }
var setArrCmd = &cobra.Command{ var setArrCmd = &cobra.Command{
@@ -24,7 +37,17 @@ var setArrCmd = &cobra.Command{
Short: "set-array [key]", Short: "set-array [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
if arrFile != "" {
arrJSONBytes, err := os.ReadFile(arrFile)
if err != nil {
return fmt.Errorf("reading array value file: %w", err)
}
err = json.Unmarshal(arrJSONBytes, &arrValue)
if err != nil {
return fmt.Errorf("reading value from array file: %w", err)
}
}
err := cli.PutStringArr(args[0], arrValue) err := cli.PutStringArr(args[0], arrValue)
if err != nil { if err != nil {
return err return err
+26 -3
View File
@@ -1,22 +1,35 @@
package hmsecrets package hmsecrets
import ( import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var mapValue map[string]string var mapValue map[string]string
var mapFile string
func init() { func init() {
rootCmd.AddCommand(setMapCmd) rootCmd.AddCommand(setMapCmd)
setMapCmd.Flags().StringToStringVarP( setMapCmd.Flags().StringToStringVarP(
&mapValue, &mapValue,
"map-value", "value",
"v", "v",
nil, nil,
"map value to store in the secret", "map value to store in the secret",
) )
setArrCmd.MarkFlagRequired("map-value") setMapCmd.Flags().StringVarP(
&mapFile,
"value-file",
"f",
"",
"file that contains the secret value (in json) to set",
)
setMapCmd.MarkFlagsOneRequired("value-file", "value")
} }
var setMapCmd = &cobra.Command{ var setMapCmd = &cobra.Command{
@@ -24,7 +37,17 @@ var setMapCmd = &cobra.Command{
Short: "set-map [key]", Short: "set-map [key]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
if mapFile != "" {
mapJSONBytes, err := os.ReadFile(mapFile)
if err != nil {
return fmt.Errorf("reading map value file: %w", err)
}
err = json.Unmarshal(mapJSONBytes, &mapValue)
if err != nil {
return fmt.Errorf("reading value from map file: %w", err)
}
}
err := cli.PutStringMap(args[0], mapValue) err := cli.PutStringMap(args[0], mapValue)
if err != nil { if err != nil {
return err return err
+1 -1
View File
@@ -13,7 +13,7 @@ var setCmd = &cobra.Command{
Short: "set [key] [value]", Short: "set [key] [value]",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cli := getClient(apiBase, caFile, clientCert, clientKey) cli := getClient()
err := cli.PutString(args[0], args[1]) err := cli.PutString(args[0], args[1])
if err != nil { if err != nil {
return err return err
+1
View File
@@ -2,3 +2,4 @@
go = "1.27.2" go = "1.27.2"
"go:golang.org/x/tools/cmd/goimports" = "latest" "go:golang.org/x/tools/cmd/goimports" = "latest"
"go:golang.org/x/tools/gopls" = "latest" "go:golang.org/x/tools/gopls" = "latest"
goreleaser = "2.19.0"