From 886e8aa11e6c9d90b3bc0b8cc46c0fb3ddc3561c Mon Sep 17 00:00:00 2001 From: Jared Allard Date: Sat, 22 Feb 2025 22:11:11 -0800 Subject: [PATCH] feat: key generation, basic CLI --- .goreleaser.yaml | 28 ++++ cmd/klefkictl/klefkictl.go | 49 +++++++ cmd/klefkictl/klefkictl_delete.go | 43 ++++++ cmd/klefkictl/klefkictl_list.go | 65 +++++++++ cmd/klefkictl/klefkictl_new.go | 68 ++++++++++ go.mod | 6 +- go.sum | 6 + internal/db/db.go | 45 +++++++ internal/db/ent/machine.go | 23 +++- internal/db/ent/machine/machine.go | 14 +- internal/db/ent/machine/where.go | 93 +++++++++---- internal/db/ent/machine_create.go | 37 +++++- internal/db/ent/machine_query.go | 4 +- internal/db/ent/machine_update.go | 42 ++++-- internal/db/ent/migrate/schema.go | 3 +- internal/db/ent/mutation.go | 68 +++++++++- internal/db/ent/runtime.go | 11 ++ internal/db/ent/schema/machine.go | 5 +- internal/machines/machine.go | 123 ++++++++++++++++++ internal/machines/machines.go | 20 +++ .../generated/go/rgst/klefki/v1/kelfki.pb.go | 5 +- .../go/rgst/klefki/v1/kelfki_grpc.pb.go | 1 - internal/server/server.go | 8 +- stencil.lock | 3 + stencil.yaml | 3 + 25 files changed, 709 insertions(+), 64 deletions(-) create mode 100644 cmd/klefkictl/klefkictl.go create mode 100644 cmd/klefkictl/klefkictl_delete.go create mode 100644 cmd/klefkictl/klefkictl_list.go create mode 100644 cmd/klefkictl/klefkictl_new.go create mode 100644 internal/db/db.go create mode 100644 internal/machines/machine.go create mode 100644 internal/machines/machines.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9bbc625..81a86d7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -28,6 +28,34 @@ builds: - windows ## <> + ## <> + ignore: + - goos: windows + goarch: arm + mod_timestamp: "{{ .CommitTimestamp }}" + - main: ./cmd/klefkictl + flags: + - -trimpath + ldflags: + - -s + - -w + ## <> + + ## <> + env: + - CGO_ENABLED=0 + goarch: + - amd64 + - arm64 + ## <> + + ## <> + goos: + - linux + - darwin + - windows + ## <> + ## <> ignore: - goos: windows diff --git a/cmd/klefkictl/klefkictl.go b/cmd/klefkictl/klefkictl.go new file mode 100644 index 0000000..0384e38 --- /dev/null +++ b/cmd/klefkictl/klefkictl.go @@ -0,0 +1,49 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/spf13/cobra" +) + +func main() { + exitCode := 0 + defer func() { os.Exit(exitCode) }() + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + + rootCmd := &cobra.Command{ + Use: "klefkictl", + Short: "CLI for interacting with klefki", + } + rootCmd.AddCommand( + newNewCommand(), + newListCommand(), + newDeleteCommand(), + ) + if err := rootCmd.ExecuteContext(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) + exitCode = 1 + } +} diff --git a/cmd/klefkictl/klefkictl_delete.go b/cmd/klefkictl/klefkictl_delete.go new file mode 100644 index 0000000..59a4dd1 --- /dev/null +++ b/cmd/klefkictl/klefkictl_delete.go @@ -0,0 +1,43 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package main + +import ( + "fmt" + + "git.rgst.io/homelab/klefki/internal/db" + "github.com/spf13/cobra" +) + +// newDeleteCommand creates a dekete [cobra.Command] +func newDeleteCommand() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a known machine by fingerprint", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + db, err := db.New(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to open DB: %w", err) + } + defer db.Close() + + return db.Machine.DeleteOneID(args[0]).Exec(cmd.Context()) + }, + } +} diff --git a/cmd/klefkictl/klefkictl_list.go b/cmd/klefkictl/klefkictl_list.go new file mode 100644 index 0000000..8941b11 --- /dev/null +++ b/cmd/klefkictl/klefkictl_list.go @@ -0,0 +1,65 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package main + +import ( + "fmt" + "os" + "text/tabwriter" + "time" + + "git.rgst.io/homelab/klefki/internal/db" + "github.com/spf13/cobra" +) + +// newListCommand creates a list [cobra.Command] +func newListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all known machines", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + db, err := db.New(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to open DB: %w", err) + } + defer db.Close() + + ms, err := db.Machine.Query().All(cmd.Context()) + if err != nil { + return err + } + if len(ms) == 0 { + fmt.Println("No results found") + return nil + } + + tw := tabwriter.NewWriter(os.Stdout, 2, 2, 2, ' ', 0) + fmt.Fprint(tw, "FINGERPRINT\tCREATED AT\n") + for _, m := range ms { + createdAt, err := time.Parse(time.RFC3339, m.CreatedAt) + if err != nil { + return fmt.Errorf("failed to parse created_at (%s): %w", m.CreatedAt, err) + } + + fmt.Fprintf(tw, "%s\t%s\n", m.ID, createdAt.Local()) + } + return tw.Flush() + }, + } +} diff --git a/cmd/klefkictl/klefkictl_new.go b/cmd/klefkictl/klefkictl_new.go new file mode 100644 index 0000000..f32b55e --- /dev/null +++ b/cmd/klefkictl/klefkictl_new.go @@ -0,0 +1,68 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package main + +import ( + "fmt" + + "git.rgst.io/homelab/klefki/internal/db" + "git.rgst.io/homelab/klefki/internal/machines" + "github.com/spf13/cobra" +) + +// newNewCommand creates a new [cobra.Command] +func newNewCommand() *cobra.Command { + return &cobra.Command{ + Use: "new", + Short: "Create a new machine", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + db, err := db.New(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to open DB: %w", err) + } + defer db.Close() + + m, err := machines.NewMachine() + if err != nil { + return err + } + + fprint, err := m.Fingerprint() + if err != nil { + return err + } + + privKey, err := m.EncodePrivateKey() + if err != nil { + return err + } + + if err := db.Machine.Create(). + SetID(fprint).SetPublicKey(m.PublicKey). + Exec(cmd.Context()); err != nil { + return fmt.Errorf("failed to write to DB: %w", err) + } + + fmt.Println("Fingerprint:", fprint) + fmt.Println("Private Key:") + fmt.Println(privKey) + return nil + }, + } +} diff --git a/go.mod b/go.mod index 18b4620..d8cd9de 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.23 require ( entgo.io/ent v0.14.2 + github.com/davecgh/go-spew v1.1.1 + github.com/ncruces/go-sqlite3 v0.23.1 + github.com/spf13/cobra v1.9.1 google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.5 ) @@ -20,10 +23,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/ncruces/julianday v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/zclconf/go-cty v1.16.2 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect golang.org/x/mod v0.23.0 // indirect diff --git a/go.sum b/go.sum index 9e6b74d..3151b58 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,10 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/ncruces/go-sqlite3 v0.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY= +github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE= +github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= +github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -51,6 +55,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..328c3a5 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,45 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +// Package db contains the DB glue logic. +package db + +import ( + "context" + "fmt" + + "entgo.io/ent/dialect" + "git.rgst.io/homelab/klefki/internal/db/ent" + + _ "github.com/ncruces/go-sqlite3/driver" // Used by ent. + _ "github.com/ncruces/go-sqlite3/embed" // Also used by ent. +) + +// New creates a new connection to the DB. +func New(ctx context.Context) (*ent.Client, error) { + client, err := ent.Open(dialect.SQLite, "file:data/klefkictl.db") + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // Run the automatic migration tool to create all schema resources. + if err := client.Schema.Create(ctx); err != nil { + return nil, fmt.Errorf("failed to run DB migrations: %w", err) + } + + return client, nil +} diff --git a/internal/db/ent/machine.go b/internal/db/ent/machine.go index 4502f52..e48dc97 100644 --- a/internal/db/ent/machine.go +++ b/internal/db/ent/machine.go @@ -18,7 +18,9 @@ type Machine struct { // Fingerprint of the public key ID string `json:"id,omitempty"` // Public key of the machine - PublicKey string `json:"public_key,omitempty"` + PublicKey []byte `json:"public_key,omitempty"` + // When this machine was added in UTC + CreatedAt string `json:"created_at,omitempty"` selectValues sql.SelectValues } @@ -27,7 +29,9 @@ func (*Machine) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case machine.FieldID, machine.FieldPublicKey: + case machine.FieldPublicKey: + values[i] = new([]byte) + case machine.FieldID, machine.FieldCreatedAt: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -51,10 +55,16 @@ func (m *Machine) assignValues(columns []string, values []any) error { m.ID = value.String } case machine.FieldPublicKey: - if value, ok := values[i].(*sql.NullString); !ok { + if value, ok := values[i].(*[]byte); !ok { return fmt.Errorf("unexpected type %T for field public_key", values[i]) + } else if value != nil { + m.PublicKey = *value + } + case machine.FieldCreatedAt: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) } else if value.Valid { - m.PublicKey = value.String + m.CreatedAt = value.String } default: m.selectValues.Set(columns[i], values[i]) @@ -93,7 +103,10 @@ func (m *Machine) String() string { builder.WriteString("Machine(") builder.WriteString(fmt.Sprintf("id=%v, ", m.ID)) builder.WriteString("public_key=") - builder.WriteString(m.PublicKey) + builder.WriteString(fmt.Sprintf("%v", m.PublicKey)) + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(m.CreatedAt) builder.WriteByte(')') return builder.String() } diff --git a/internal/db/ent/machine/machine.go b/internal/db/ent/machine/machine.go index b40116e..bf4b802 100644 --- a/internal/db/ent/machine/machine.go +++ b/internal/db/ent/machine/machine.go @@ -13,6 +13,8 @@ const ( FieldID = "id" // FieldPublicKey holds the string denoting the public_key field in the database. FieldPublicKey = "public_key" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" // Table holds the table name of the machine in the database. Table = "machines" ) @@ -21,6 +23,7 @@ const ( var Columns = []string{ FieldID, FieldPublicKey, + FieldCreatedAt, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -33,6 +36,11 @@ func ValidColumn(column string) bool { return false } +var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt string +) + // OrderOption defines the ordering options for the Machine queries. type OrderOption func(*sql.Selector) @@ -41,7 +49,7 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } -// ByPublicKey orders the results by the public_key field. -func ByPublicKey(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldPublicKey, opts...).ToFunc() +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() } diff --git a/internal/db/ent/machine/where.go b/internal/db/ent/machine/where.go index 601fb09..da07094 100644 --- a/internal/db/ent/machine/where.go +++ b/internal/db/ent/machine/where.go @@ -63,73 +63,118 @@ func IDContainsFold(id string) predicate.Machine { } // PublicKey applies equality check predicate on the "public_key" field. It's identical to PublicKeyEQ. -func PublicKey(v string) predicate.Machine { +func PublicKey(v []byte) predicate.Machine { return predicate.Machine(sql.FieldEQ(FieldPublicKey, v)) } +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v string) predicate.Machine { + return predicate.Machine(sql.FieldEQ(FieldCreatedAt, v)) +} + // PublicKeyEQ applies the EQ predicate on the "public_key" field. -func PublicKeyEQ(v string) predicate.Machine { +func PublicKeyEQ(v []byte) predicate.Machine { return predicate.Machine(sql.FieldEQ(FieldPublicKey, v)) } // PublicKeyNEQ applies the NEQ predicate on the "public_key" field. -func PublicKeyNEQ(v string) predicate.Machine { +func PublicKeyNEQ(v []byte) predicate.Machine { return predicate.Machine(sql.FieldNEQ(FieldPublicKey, v)) } // PublicKeyIn applies the In predicate on the "public_key" field. -func PublicKeyIn(vs ...string) predicate.Machine { +func PublicKeyIn(vs ...[]byte) predicate.Machine { return predicate.Machine(sql.FieldIn(FieldPublicKey, vs...)) } // PublicKeyNotIn applies the NotIn predicate on the "public_key" field. -func PublicKeyNotIn(vs ...string) predicate.Machine { +func PublicKeyNotIn(vs ...[]byte) predicate.Machine { return predicate.Machine(sql.FieldNotIn(FieldPublicKey, vs...)) } // PublicKeyGT applies the GT predicate on the "public_key" field. -func PublicKeyGT(v string) predicate.Machine { +func PublicKeyGT(v []byte) predicate.Machine { return predicate.Machine(sql.FieldGT(FieldPublicKey, v)) } // PublicKeyGTE applies the GTE predicate on the "public_key" field. -func PublicKeyGTE(v string) predicate.Machine { +func PublicKeyGTE(v []byte) predicate.Machine { return predicate.Machine(sql.FieldGTE(FieldPublicKey, v)) } // PublicKeyLT applies the LT predicate on the "public_key" field. -func PublicKeyLT(v string) predicate.Machine { +func PublicKeyLT(v []byte) predicate.Machine { return predicate.Machine(sql.FieldLT(FieldPublicKey, v)) } // PublicKeyLTE applies the LTE predicate on the "public_key" field. -func PublicKeyLTE(v string) predicate.Machine { +func PublicKeyLTE(v []byte) predicate.Machine { return predicate.Machine(sql.FieldLTE(FieldPublicKey, v)) } -// PublicKeyContains applies the Contains predicate on the "public_key" field. -func PublicKeyContains(v string) predicate.Machine { - return predicate.Machine(sql.FieldContains(FieldPublicKey, v)) +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v string) predicate.Machine { + return predicate.Machine(sql.FieldEQ(FieldCreatedAt, v)) } -// PublicKeyHasPrefix applies the HasPrefix predicate on the "public_key" field. -func PublicKeyHasPrefix(v string) predicate.Machine { - return predicate.Machine(sql.FieldHasPrefix(FieldPublicKey, v)) +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v string) predicate.Machine { + return predicate.Machine(sql.FieldNEQ(FieldCreatedAt, v)) } -// PublicKeyHasSuffix applies the HasSuffix predicate on the "public_key" field. -func PublicKeyHasSuffix(v string) predicate.Machine { - return predicate.Machine(sql.FieldHasSuffix(FieldPublicKey, v)) +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...string) predicate.Machine { + return predicate.Machine(sql.FieldIn(FieldCreatedAt, vs...)) } -// PublicKeyEqualFold applies the EqualFold predicate on the "public_key" field. -func PublicKeyEqualFold(v string) predicate.Machine { - return predicate.Machine(sql.FieldEqualFold(FieldPublicKey, v)) +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...string) predicate.Machine { + return predicate.Machine(sql.FieldNotIn(FieldCreatedAt, vs...)) } -// PublicKeyContainsFold applies the ContainsFold predicate on the "public_key" field. -func PublicKeyContainsFold(v string) predicate.Machine { - return predicate.Machine(sql.FieldContainsFold(FieldPublicKey, v)) +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v string) predicate.Machine { + return predicate.Machine(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v string) predicate.Machine { + return predicate.Machine(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v string) predicate.Machine { + return predicate.Machine(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v string) predicate.Machine { + return predicate.Machine(sql.FieldLTE(FieldCreatedAt, v)) +} + +// CreatedAtContains applies the Contains predicate on the "created_at" field. +func CreatedAtContains(v string) predicate.Machine { + return predicate.Machine(sql.FieldContains(FieldCreatedAt, v)) +} + +// CreatedAtHasPrefix applies the HasPrefix predicate on the "created_at" field. +func CreatedAtHasPrefix(v string) predicate.Machine { + return predicate.Machine(sql.FieldHasPrefix(FieldCreatedAt, v)) +} + +// CreatedAtHasSuffix applies the HasSuffix predicate on the "created_at" field. +func CreatedAtHasSuffix(v string) predicate.Machine { + return predicate.Machine(sql.FieldHasSuffix(FieldCreatedAt, v)) +} + +// CreatedAtEqualFold applies the EqualFold predicate on the "created_at" field. +func CreatedAtEqualFold(v string) predicate.Machine { + return predicate.Machine(sql.FieldEqualFold(FieldCreatedAt, v)) +} + +// CreatedAtContainsFold applies the ContainsFold predicate on the "created_at" field. +func CreatedAtContainsFold(v string) predicate.Machine { + return predicate.Machine(sql.FieldContainsFold(FieldCreatedAt, v)) } // And groups predicates with the AND operator between them. diff --git a/internal/db/ent/machine_create.go b/internal/db/ent/machine_create.go index 66aa732..dc89799 100644 --- a/internal/db/ent/machine_create.go +++ b/internal/db/ent/machine_create.go @@ -20,8 +20,22 @@ type MachineCreate struct { } // SetPublicKey sets the "public_key" field. -func (mc *MachineCreate) SetPublicKey(s string) *MachineCreate { - mc.mutation.SetPublicKey(s) +func (mc *MachineCreate) SetPublicKey(b []byte) *MachineCreate { + mc.mutation.SetPublicKey(b) + return mc +} + +// SetCreatedAt sets the "created_at" field. +func (mc *MachineCreate) SetCreatedAt(s string) *MachineCreate { + mc.mutation.SetCreatedAt(s) + return mc +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (mc *MachineCreate) SetNillableCreatedAt(s *string) *MachineCreate { + if s != nil { + mc.SetCreatedAt(*s) + } return mc } @@ -38,6 +52,7 @@ func (mc *MachineCreate) Mutation() *MachineMutation { // Save creates the Machine in the database. func (mc *MachineCreate) Save(ctx context.Context) (*Machine, error) { + mc.defaults() return withHooks(ctx, mc.sqlSave, mc.mutation, mc.hooks) } @@ -63,11 +78,22 @@ func (mc *MachineCreate) ExecX(ctx context.Context) { } } +// defaults sets the default values of the builder before save. +func (mc *MachineCreate) defaults() { + if _, ok := mc.mutation.CreatedAt(); !ok { + v := machine.DefaultCreatedAt + mc.mutation.SetCreatedAt(v) + } +} + // check runs all checks and user-defined validators on the builder. func (mc *MachineCreate) check() error { if _, ok := mc.mutation.PublicKey(); !ok { return &ValidationError{Name: "public_key", err: errors.New(`ent: missing required field "Machine.public_key"`)} } + if _, ok := mc.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Machine.created_at"`)} + } return nil } @@ -104,9 +130,13 @@ func (mc *MachineCreate) createSpec() (*Machine, *sqlgraph.CreateSpec) { _spec.ID.Value = id } if value, ok := mc.mutation.PublicKey(); ok { - _spec.SetField(machine.FieldPublicKey, field.TypeString, value) + _spec.SetField(machine.FieldPublicKey, field.TypeBytes, value) _node.PublicKey = value } + if value, ok := mc.mutation.CreatedAt(); ok { + _spec.SetField(machine.FieldCreatedAt, field.TypeString, value) + _node.CreatedAt = value + } return _node, _spec } @@ -128,6 +158,7 @@ func (mcb *MachineCreateBulk) Save(ctx context.Context) ([]*Machine, error) { for i := range mcb.builders { func(i int, root context.Context) { builder := mcb.builders[i] + builder.defaults() var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { mutation, ok := m.(*MachineMutation) if !ok { diff --git a/internal/db/ent/machine_query.go b/internal/db/ent/machine_query.go index d5d4413..1637396 100644 --- a/internal/db/ent/machine_query.go +++ b/internal/db/ent/machine_query.go @@ -262,7 +262,7 @@ func (mq *MachineQuery) Clone() *MachineQuery { // Example: // // var v []struct { -// PublicKey string `json:"public_key,omitempty"` +// PublicKey []byte `json:"public_key,omitempty"` // Count int `json:"count,omitempty"` // } // @@ -285,7 +285,7 @@ func (mq *MachineQuery) GroupBy(field string, fields ...string) *MachineGroupBy // Example: // // var v []struct { -// PublicKey string `json:"public_key,omitempty"` +// PublicKey []byte `json:"public_key,omitempty"` // } // // client.Machine.Query(). diff --git a/internal/db/ent/machine_update.go b/internal/db/ent/machine_update.go index e1b41b2..3ddb6af 100644 --- a/internal/db/ent/machine_update.go +++ b/internal/db/ent/machine_update.go @@ -28,15 +28,21 @@ func (mu *MachineUpdate) Where(ps ...predicate.Machine) *MachineUpdate { } // SetPublicKey sets the "public_key" field. -func (mu *MachineUpdate) SetPublicKey(s string) *MachineUpdate { - mu.mutation.SetPublicKey(s) +func (mu *MachineUpdate) SetPublicKey(b []byte) *MachineUpdate { + mu.mutation.SetPublicKey(b) return mu } -// SetNillablePublicKey sets the "public_key" field if the given value is not nil. -func (mu *MachineUpdate) SetNillablePublicKey(s *string) *MachineUpdate { +// SetCreatedAt sets the "created_at" field. +func (mu *MachineUpdate) SetCreatedAt(s string) *MachineUpdate { + mu.mutation.SetCreatedAt(s) + return mu +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (mu *MachineUpdate) SetNillableCreatedAt(s *string) *MachineUpdate { if s != nil { - mu.SetPublicKey(*s) + mu.SetCreatedAt(*s) } return mu } @@ -83,7 +89,10 @@ func (mu *MachineUpdate) sqlSave(ctx context.Context) (n int, err error) { } } if value, ok := mu.mutation.PublicKey(); ok { - _spec.SetField(machine.FieldPublicKey, field.TypeString, value) + _spec.SetField(machine.FieldPublicKey, field.TypeBytes, value) + } + if value, ok := mu.mutation.CreatedAt(); ok { + _spec.SetField(machine.FieldCreatedAt, field.TypeString, value) } if n, err = sqlgraph.UpdateNodes(ctx, mu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -106,15 +115,21 @@ type MachineUpdateOne struct { } // SetPublicKey sets the "public_key" field. -func (muo *MachineUpdateOne) SetPublicKey(s string) *MachineUpdateOne { - muo.mutation.SetPublicKey(s) +func (muo *MachineUpdateOne) SetPublicKey(b []byte) *MachineUpdateOne { + muo.mutation.SetPublicKey(b) return muo } -// SetNillablePublicKey sets the "public_key" field if the given value is not nil. -func (muo *MachineUpdateOne) SetNillablePublicKey(s *string) *MachineUpdateOne { +// SetCreatedAt sets the "created_at" field. +func (muo *MachineUpdateOne) SetCreatedAt(s string) *MachineUpdateOne { + muo.mutation.SetCreatedAt(s) + return muo +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (muo *MachineUpdateOne) SetNillableCreatedAt(s *string) *MachineUpdateOne { if s != nil { - muo.SetPublicKey(*s) + muo.SetCreatedAt(*s) } return muo } @@ -191,7 +206,10 @@ func (muo *MachineUpdateOne) sqlSave(ctx context.Context) (_node *Machine, err e } } if value, ok := muo.mutation.PublicKey(); ok { - _spec.SetField(machine.FieldPublicKey, field.TypeString, value) + _spec.SetField(machine.FieldPublicKey, field.TypeBytes, value) + } + if value, ok := muo.mutation.CreatedAt(); ok { + _spec.SetField(machine.FieldCreatedAt, field.TypeString, value) } _node = &Machine{config: muo.config} _spec.Assign = _node.assignValues diff --git a/internal/db/ent/migrate/schema.go b/internal/db/ent/migrate/schema.go index e93e0b2..e800f30 100644 --- a/internal/db/ent/migrate/schema.go +++ b/internal/db/ent/migrate/schema.go @@ -11,7 +11,8 @@ var ( // MachinesColumns holds the columns for the "machines" table. MachinesColumns = []*schema.Column{ {Name: "id", Type: field.TypeString}, - {Name: "public_key", Type: field.TypeString}, + {Name: "public_key", Type: field.TypeBytes}, + {Name: "created_at", Type: field.TypeString, Default: "2025-02-23T06:06:02Z"}, } // MachinesTable holds the schema information for the "machines" table. MachinesTable = &schema.Table{ diff --git a/internal/db/ent/mutation.go b/internal/db/ent/mutation.go index 95cfa7a..c4da0fe 100644 --- a/internal/db/ent/mutation.go +++ b/internal/db/ent/mutation.go @@ -32,7 +32,8 @@ type MachineMutation struct { op Op typ string id *string - public_key *string + public_key *[]byte + created_at *string clearedFields map[string]struct{} done bool oldValue func(context.Context) (*Machine, error) @@ -144,12 +145,12 @@ func (m *MachineMutation) IDs(ctx context.Context) ([]string, error) { } // SetPublicKey sets the "public_key" field. -func (m *MachineMutation) SetPublicKey(s string) { - m.public_key = &s +func (m *MachineMutation) SetPublicKey(b []byte) { + m.public_key = &b } // PublicKey returns the value of the "public_key" field in the mutation. -func (m *MachineMutation) PublicKey() (r string, exists bool) { +func (m *MachineMutation) PublicKey() (r []byte, exists bool) { v := m.public_key if v == nil { return @@ -160,7 +161,7 @@ func (m *MachineMutation) PublicKey() (r string, exists bool) { // OldPublicKey returns the old "public_key" field's value of the Machine entity. // If the Machine object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MachineMutation) OldPublicKey(ctx context.Context) (v string, err error) { +func (m *MachineMutation) OldPublicKey(ctx context.Context) (v []byte, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldPublicKey is only allowed on UpdateOne operations") } @@ -179,6 +180,42 @@ func (m *MachineMutation) ResetPublicKey() { m.public_key = nil } +// SetCreatedAt sets the "created_at" field. +func (m *MachineMutation) SetCreatedAt(s string) { + m.created_at = &s +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *MachineMutation) CreatedAt() (r string, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the Machine entity. +// If the Machine object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MachineMutation) OldCreatedAt(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *MachineMutation) ResetCreatedAt() { + m.created_at = nil +} + // Where appends a list predicates to the MachineMutation builder. func (m *MachineMutation) Where(ps ...predicate.Machine) { m.predicates = append(m.predicates, ps...) @@ -213,10 +250,13 @@ func (m *MachineMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *MachineMutation) Fields() []string { - fields := make([]string, 0, 1) + fields := make([]string, 0, 2) if m.public_key != nil { fields = append(fields, machine.FieldPublicKey) } + if m.created_at != nil { + fields = append(fields, machine.FieldCreatedAt) + } return fields } @@ -227,6 +267,8 @@ func (m *MachineMutation) Field(name string) (ent.Value, bool) { switch name { case machine.FieldPublicKey: return m.PublicKey() + case machine.FieldCreatedAt: + return m.CreatedAt() } return nil, false } @@ -238,6 +280,8 @@ func (m *MachineMutation) OldField(ctx context.Context, name string) (ent.Value, switch name { case machine.FieldPublicKey: return m.OldPublicKey(ctx) + case machine.FieldCreatedAt: + return m.OldCreatedAt(ctx) } return nil, fmt.Errorf("unknown Machine field %s", name) } @@ -248,12 +292,19 @@ func (m *MachineMutation) OldField(ctx context.Context, name string) (ent.Value, func (m *MachineMutation) SetField(name string, value ent.Value) error { switch name { case machine.FieldPublicKey: - v, ok := value.(string) + v, ok := value.([]byte) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } m.SetPublicKey(v) return nil + case machine.FieldCreatedAt: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil } return fmt.Errorf("unknown Machine field %s", name) } @@ -306,6 +357,9 @@ func (m *MachineMutation) ResetField(name string) error { case machine.FieldPublicKey: m.ResetPublicKey() return nil + case machine.FieldCreatedAt: + m.ResetCreatedAt() + return nil } return fmt.Errorf("unknown Machine field %s", name) } diff --git a/internal/db/ent/runtime.go b/internal/db/ent/runtime.go index 793d053..56d9347 100644 --- a/internal/db/ent/runtime.go +++ b/internal/db/ent/runtime.go @@ -2,8 +2,19 @@ package ent +import ( + "git.rgst.io/homelab/klefki/internal/db/ent/machine" + "git.rgst.io/homelab/klefki/internal/db/ent/schema" +) + // The init function reads all schema descriptors with runtime code // (default values, validators, hooks and policies) and stitches it // to their package variables. func init() { + machineFields := schema.Machine{}.Fields() + _ = machineFields + // machineDescCreatedAt is the schema descriptor for created_at field. + machineDescCreatedAt := machineFields[2].Descriptor() + // machine.DefaultCreatedAt holds the default value on creation for the created_at field. + machine.DefaultCreatedAt = machineDescCreatedAt.Default.(string) } diff --git a/internal/db/ent/schema/machine.go b/internal/db/ent/schema/machine.go index fdd735e..be4f241 100644 --- a/internal/db/ent/schema/machine.go +++ b/internal/db/ent/schema/machine.go @@ -18,6 +18,8 @@ package schema import ( + "time" + "entgo.io/ent" "entgo.io/ent/schema/field" ) @@ -31,6 +33,7 @@ type Machine struct { func (Machine) Fields() []ent.Field { return []ent.Field{ field.String("id").Comment("Fingerprint of the public key"), - field.String("public_key").Comment("Public key of the machine"), + field.Bytes("public_key").Comment("Public key of the machine"), + field.String("created_at").Comment("When this machine was added in UTC").Default(time.Now().UTC().Format(time.RFC3339)), } } diff --git a/internal/machines/machine.go b/internal/machines/machine.go new file mode 100644 index 0000000..7d289df --- /dev/null +++ b/internal/machines/machine.go @@ -0,0 +1,123 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +package machines + +import ( + "crypto/ed25519" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "sync" + + "git.rgst.io/homelab/klefki/internal/db/ent" +) + +// getFingerprint returns a fingerprint of the key. +func getFingerprint(pub ed25519.PublicKey) (string, error) { + hasher := sha256.New() + if _, err := hasher.Write(pub); err != nil { + return "", fmt.Errorf("failed to hash provided public key: %w", err) + } + return "SHA256:" + base64.RawStdEncoding.EncodeToString(hasher.Sum(nil)), nil +} + +// Machine is a known machine containing PKI used by it. +type Machine struct { + fprintOnce sync.Once + fingerprint string + + // PublicKey is the public key for this machine. This is always set + // when initialized through [MachineFromDB] or [Machine]. + PublicKey ed25519.PublicKey + + // PrivateKye is the private key for this machine. This is normally + // not set instead only when [NewMachine] is called. + PrivateKey ed25519.PrivateKey +} + +// String returns a string version of the machine containing only the +// fingerprint, if obtainable. +func (m *Machine) String() string { + fprint, err := m.Fingerprint() + if err != nil { + fprint = fmt.Sprintf("", err) + } + + return "Machine<" + fprint + ">" +} + +// Fingerprint returns the fingerprint of the machine as calculated from +// the public key. This is calculated exactly once. If m.fingerprint is +// already set, this immediately returns that value instead of +// calculating it. +func (m *Machine) Fingerprint() (string, error) { + var err error + m.fprintOnce.Do(func() { + if m.fingerprint != "" { + return // NOOP if already set. + } + m.fingerprint, err = getFingerprint(m.PublicKey) + }) + if err != nil { + return "", fmt.Errorf("failed to calculate fingerprint: %w", err) + } + + return m.fingerprint, nil +} + +// EncodePrivateKey returns a X509 PEM encoded private key for the +// ed25519 private key of this machine. +func (m *Machine) EncodePrivateKey() (string, error) { + privKey, err := x509.MarshalPKCS8PrivateKey(m.PrivateKey) + if err != nil { + return "", fmt.Errorf("failed to marshal private key: %w", err) + } + + encoded := pem.EncodeToMemory(&pem.Block{Type: "ED25519 PRIVATE KEY", Bytes: privKey}) + return string(encoded), nil +} + +// EncodePublicKey returns a X509 PEM encoded public key for the +// ed25519 public key of this machine. +func (m *Machine) EncodePublicKey() (string, error) { + privKey, err := x509.MarshalPKIXPublicKey(m.PublicKey) + if err != nil { + return "", fmt.Errorf("failed to marshal public key: %w", err) + } + + encoded := pem.EncodeToMemory(&pem.Block{Type: "ED25519 PUBLIC KEY", Bytes: privKey}) + return string(encoded), nil +} + +// MachineFromDB creates a [Machine] from a [ent.Machine]. Note that the +// private key will never be set in this case as it is no longer known. +func MachineFromDB(m *ent.Machine) *Machine { + return &Machine{fingerprint: m.ID, PublicKey: ed25519.PublicKey(m.PublicKey)} +} + +// NewMachine creates a new [Machine] with the private key included. +func NewMachine() (*Machine, error) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, fmt.Errorf("failed to generate ed25519 key: %w", err) + } + + return &Machine{PublicKey: pub, PrivateKey: priv}, nil +} diff --git a/internal/machines/machines.go b/internal/machines/machines.go new file mode 100644 index 0000000..b3ef287 --- /dev/null +++ b/internal/machines/machines.go @@ -0,0 +1,20 @@ +// Copyright (C) 2025 klefki contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: AGPL-3.0 + +// Package machines contains all of the code for creating and managing +// machines. +package machines diff --git a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go index 0ba5141..8a94790 100644 --- a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go +++ b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki.pb.go @@ -7,12 +7,11 @@ package v1 import ( - reflect "reflect" - unsafe "unsafe" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" _ "google.golang.org/protobuf/types/gofeaturespb" + reflect "reflect" + unsafe "unsafe" ) const ( diff --git a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki_grpc.pb.go b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki_grpc.pb.go index 5514b65..f7eb586 100644 --- a/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki_grpc.pb.go +++ b/internal/server/grpc/generated/go/rgst/klefki/v1/kelfki_grpc.pb.go @@ -8,7 +8,6 @@ package v1 import ( context "context" - grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/internal/server/server.go b/internal/server/server.go index 50422f6..cf1a682 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -46,10 +46,16 @@ func (s *Server) Run(_ context.Context) error { } fmt.Printf("starting gRPC server on %s\n", lis.Addr()) - return s.gs.Serve(lis) } +// GetKey implements the GetKey request +func (s *Server) GetKey(_ context.Context, _ *pbgrpcv1.GetKeyRequest) (*pbgrpcv1.GetKeyResponse, error) { + resp := &pbgrpcv1.GetKeyResponse{} + resp.SetKey("hello-world") + return resp, nil +} + // Close closes the server func (s *Server) Close(_ context.Context) error { if s.gs == nil { diff --git a/stencil.lock b/stencil.lock index dd7b25d..59409e3 100644 --- a/stencil.lock +++ b/stencil.lock @@ -57,6 +57,9 @@ files: - name: cmd/klefki/klefki.go template: cmd/$name/name.go.tpl module: github.com/rgst-io/stencil-golang + - name: cmd/klefkictl/klefkictl.go + template: cmd/$name/name.go.tpl + module: github.com/rgst-io/stencil-golang - name: go.mod template: go.mod.tpl module: github.com/rgst-io/stencil-golang diff --git a/stencil.yaml b/stencil.yaml index bcf1391..ae0ea54 100644 --- a/stencil.yaml +++ b/stencil.yaml @@ -4,5 +4,8 @@ arguments: vcs_host: git.rgst.io org: homelab license: AGPL-3.0 + commands: + - klefki + - klefkictl modules: - name: github.com/rgst-io/stencil-golang