Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OTS-CLI utility #117

Merged
merged 6 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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