Skip to content

Commit

Permalink
Implement OTS-CLI utility (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
Luzifer authored Oct 4, 2023
1 parent c512473 commit 546481d
Show file tree
Hide file tree
Showing 20 changed files with 861 additions and 70 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/test-and-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,19 @@ jobs:
- name: Marking workdir safe
run: git config --global --add safe.directory /__w/ots/ots

- name: Lint and test code
- name: 'Lint and test code: API'
run: |
go test -v ./...
go test -cover -v ./...
- name: 'Lint and test code: Client'
working-directory: ./pkg/client
run: |
go test -cover -v ./...
- name: 'Lint and test code: OTS-CLI'
working-directory: ./cmd/ots-cli
run: |
go test -cover -v ./...
- name: Generate (and validate) translations
run: make translate
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.build
customize.yaml
frontend/api.html
frontend/app.css
Expand Down
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ generate-inner:
node ./ci/build.mjs

publish: download_libs generate-inner generate-apidocs
curl -sSLo golang.sh https://raw.githubusercontent.com/Luzifer/github-publish/master/golang.sh
bash golang.sh
bash ./ci/build.sh

translate:
cd ci/translate && go run .
Expand Down
66 changes: 66 additions & 0 deletions ci/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

osarch=(
darwin/amd64
darwin/arm64
linux/amd64
linux/arm
linux/arm64
windows/amd64
)

function go_package() {
cd "${4}"

local outname="${3}"
[[ $1 == windows ]] && outname="${3}.exe"

log "=> Building ${3} for ${1}/${2}..."
CGO_ENABLED=0 GOARCH=$2 GOOS=$1 go build \
-ldflags "-s -w -X main.version=${version}" \
-mod=readonly \
-trimpath \
-o "${outname}"

if [[ $1 == linux ]]; then
log "=> Packging ${3} as ${3}_${1}_${2}.tgz..."
tar -czf "${builddir}/${3}_${1}_${2}.tgz" "${outname}"
else
log "=> Packging ${3} as ${3}_${1}_${2}.zip..."
zip "${builddir}/${3}_${1}_${2}.zip" "${outname}"
fi

rm "${outname}"
}

function go_package_all() {
for oa in "${osarch[@]}"; do
local os=$(cut -d / -f 1 <<<"${oa}")
local arch=$(cut -d / -f 2 <<<"${oa}")
(go_package "${os}" "${arch}" "${1}" "${2}")
done
}

function log() {
echo "[$(date +%H:%M:%S)] $@" >&2
}

root=$(pwd)
builddir="${root}/.build"
version="$(git describe --tags --always || echo dev)"

log "Building version ${version}..."

log "Resetting output directory..."
rm -rf "${builddir}"
mkdir -p "${builddir}"

log "Building API-Server..."
go_package_all "ots" "."

log "Building OTS-CLI..."
go_package_all "ots-cli" "./cmd/ots-cli"

log "Generating SHA256SUMS file..."
(cd "${builddir}" && sha256sum * | tee SHA256SUMS)
38 changes: 0 additions & 38 deletions cli_create.sh

This file was deleted.

28 changes: 0 additions & 28 deletions cli_get.sh

This file was deleted.

1 change: 1 addition & 0 deletions cmd/ots-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ots-cli
105 changes: 105 additions & 0 deletions cmd/ots-cli/cmd_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"fmt"
"io"
"mime"
"os"
"path"

"github.com/Luzifer/ots/pkg/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var createCmd = &cobra.Command{
Use: "create [-f file]... [--instance url] [--secret-from file]",
Short: "Create a new encrypted secret in the given OTS instance",
Long: "",
Example: `echo "I'm a very secret secret" | ots-cli create`,
Args: cobra.NoArgs,
RunE: createRunE,
}

func init() {
createCmd.Flags().Duration("expire", 0, "When to expire the secret (0 to use server-default)")
createCmd.Flags().String("instance", "https://ots.fyi/", "Instance to create the secret with")
createCmd.Flags().StringSliceP("file", "f", nil, "File(s) to attach to the secret")
createCmd.Flags().String("secret-from", "-", `File to read the secret content from ("-" for STDIN)`)
rootCmd.AddCommand(createCmd)
}

func createRunE(cmd *cobra.Command, _ []string) error {
var secret client.Secret

// Read the secret content
logrus.Info("reading secret content...")
secretSourceName, err := cmd.Flags().GetString("secret-from")
if err != nil {
return fmt.Errorf("getting secret-from flag: %w", err)
}

var secretSource io.Reader
if secretSourceName == "-" {
secretSource = os.Stdin
} else {
f, err := os.Open(secretSourceName) //#nosec:G304 // Opening user specified file is intended
if err != nil {
return fmt.Errorf("opening secret-from file: %w", err)
}
defer f.Close() //nolint:errcheck // The file will be force-closed by program exit
secretSource = f
}

secretContent, err := io.ReadAll(secretSource)
if err != nil {
return fmt.Errorf("reading secret content: %w", err)
}
secret.Secret = string(secretContent)

// Attach any file given
files, err := cmd.Flags().GetStringSlice("file")
if err != nil {
return fmt.Errorf("getting file flag: %w", err)
}
for _, f := range files {
logrus.WithField("file", f).Info("attaching file...")
content, err := os.ReadFile(f) //#nosec:G304 // Opening user specified file is intended
if err != nil {
return fmt.Errorf("reading attachment %q: %w", f, err)
}

secret.Attachments = append(secret.Attachments, client.SecretAttachment{
Name: f,
Type: mime.TypeByExtension(path.Ext(f)),
Content: content,
})
}

// Create the secret
logrus.Info("creating the secret...")
instanceURL, err := cmd.Flags().GetString("instance")
if err != nil {
return fmt.Errorf("getting instance flag: %w", err)
}

expire, err := cmd.Flags().GetDuration("expire")
if err != nil {
return fmt.Errorf("getting expire flag: %w", err)
}

secretURL, expiresAt, err := client.Create(instanceURL, secret, expire)
if err != nil {
return fmt.Errorf("creating secret: %w", err)
}

// Tell them where to find the secret
if expiresAt.IsZero() {
logrus.Info("secret created, see URL below")
} else {
logrus.WithField("expires-at", expiresAt).Info("secret created, see URL below")
}
fmt.Println(secretURL) //nolint:forbidigo // Output intended for STDOUT

return nil
}
96 changes: 96 additions & 0 deletions cmd/ots-cli/cmd_fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"strings"

"github.com/Luzifer/ots/pkg/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

const storeFileMode = 0o600 // We assume the attached file to be a secret

var fetchCmd = &cobra.Command{
Use: "fetch url",
Short: "Retrieves a secret from the instance by its URL",
Long: "",
Args: cobra.ExactArgs(1),
RunE: fetchRunE,
}

func init() {
fetchCmd.Flags().String("file-dir", ".", "Where to put files attached to the secret")
rootCmd.AddCommand(fetchCmd)
}

func checkDirWritable(dir string) error {
tmpFile := path.Join(dir, ".ots-cli.tmp")
if err := os.WriteFile(tmpFile, []byte(""), storeFileMode); err != nil {
return fmt.Errorf("writing tmp-file: %w", err)
}
defer os.Remove(tmpFile) //nolint:errcheck // We don't really care

return nil
}

func fetchRunE(cmd *cobra.Command, args []string) error {
fileDir, err := cmd.Flags().GetString("file-dir")
if err != nil {
return fmt.Errorf("getting file-dir parameter: %w", err)
}

// First lets check whether we potentially can write files
if err := checkDirWritable(fileDir); err != nil {
return fmt.Errorf("checking for directory write: %w", err)
}

logrus.Info("fetching secret...")
secret, err := client.Fetch(args[0])
if err != nil {
return fmt.Errorf("fetching secret")
}

for _, f := range secret.Attachments {
logrus.WithField("file", f.Name).Info("storing file...")
if err = storeAttachment(fileDir, f); err != nil {
return fmt.Errorf("saving file to disk: %w", err)
}
}

fmt.Println(secret.Secret) //nolint:forbidigo // Output intended for STDOUT

return nil
}

func storeAttachment(dir string, f client.SecretAttachment) error {
// First lets find a free file name to save the file as
var (
fileNameFragments = strings.SplitN(f.Name, ".", 2) //nolint:gomnd
i int
storeName = path.Join(dir, f.Name)
storeNameTpl string
)

if len(fileNameFragments) == 1 {
storeNameTpl = fmt.Sprintf("%s (%%d)", fileNameFragments[0])
} else {
storeNameTpl = fmt.Sprintf("%s (%%d).%s", fileNameFragments[0], fileNameFragments[1])
}

for _, err := os.Stat(storeName); !errors.Is(err, fs.ErrNotExist); _, err = os.Stat(storeName) {
i++
storeName = fmt.Sprintf(storeNameTpl, i)
}

// So we finally found a filename we can use
if err := os.WriteFile(storeName, f.Content, storeFileMode); err != nil {
return fmt.Errorf("writing file: %w", err)
}

return nil
}
Loading

0 comments on commit 546481d

Please sign in to comment.