initial commit
This commit is contained in:
commit
60fb40d61a
34 changed files with 2571 additions and 0 deletions
100
pkg/api/v1/client/client.go
Normal file
100
pkg/api/v1/client/client.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (C) 2023 Marius Schellenberger
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apiclient "git.giftfish.de/ston1th/keyctl/pkg/api/client"
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/v1/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClientIsNil = errors.New("client is nil")
|
||||
base = schema.LBPath
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
c *apiclient.Client
|
||||
}
|
||||
|
||||
func NewClient(c *apiclient.Client) (*Client, error) {
|
||||
if c == nil {
|
||||
return nil, ErrClientIsNil
|
||||
}
|
||||
return &Client{c}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetLoadBalancers(ctx context.Context) (lbs map[string]*schema.LoadBalancer, err error) {
|
||||
return c.GetLoadBalancersWithCluster(ctx, "")
|
||||
}
|
||||
|
||||
func (c *Client) GetLoadBalancersWithCluster(ctx context.Context, cluster string) (lbs map[string]*schema.LoadBalancer, err error) {
|
||||
path := base
|
||||
if cluster != "" {
|
||||
path += "/" + cluster
|
||||
}
|
||||
req, err := c.c.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lbs = make(map[string]*schema.LoadBalancer)
|
||||
_, err = c.c.Do(req, &lbs)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetLoadBalancer(ctx context.Context, cluster, name string) (lb *schema.LoadBalancer, err error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", base, cluster, name)
|
||||
req, err := c.c.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = c.c.Do(req, &lb)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) CreateLoadBalancer(ctx context.Context, cluster, name string, lb *schema.LoadBalancer) (err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
path := fmt.Sprintf("%s/%s/%s", base, cluster, name)
|
||||
err = lb.ValidateClient()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(buf).Encode(lb)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req, err := c.c.NewRequest(ctx, "POST", path, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = c.c.Do(req, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) DeleteLoadBalancer(ctx context.Context, cluster, name string) (err error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", base, cluster, name)
|
||||
req, err := c.c.NewRequest(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = c.c.Do(req, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) DeleteLoadBalancersWithCluster(ctx context.Context, cluster string) (err error) {
|
||||
if cluster == "" {
|
||||
return errors.New("cluster value can not be empty")
|
||||
}
|
||||
path := base + "/" + cluster
|
||||
req, err := c.c.NewRequest(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = c.c.Do(req, nil)
|
||||
return
|
||||
}
|
||||
95
pkg/api/v1/schema/schema.go
Normal file
95
pkg/api/v1/schema/schema.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (C) 2023 Marius Schellenberger
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/types"
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/core"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "v1"
|
||||
KeyName = "key"
|
||||
ReqName = "req"
|
||||
KeyPath = "/" + Version + "/" + KeyName
|
||||
ReqPath = "/" + Version + "/" + ReqName
|
||||
)
|
||||
|
||||
type NewKey struct {
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
Encoding core.Encoding `json:"encoding"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidKeyName = errors.New("invalid key name")
|
||||
ErrInvalidKeySize = errors.New("invalid key size")
|
||||
)
|
||||
|
||||
const (
|
||||
MinKeySize = 1
|
||||
MaxKeySize = 1024
|
||||
)
|
||||
|
||||
func (nk NewKey) Validate() error {
|
||||
if core.NameRe.MatchString(nk.Name) {
|
||||
return ErrInvalidKeyName
|
||||
}
|
||||
if nk.Size > MaxKeySize || nk.Size < MinKeySize {
|
||||
return ErrInvalidKeySize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApproveURL(id, aid string) string {
|
||||
return KeyPath + "/" + id + "/" + aid
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Created int64 `json:"created"`
|
||||
Size int `json:"size"`
|
||||
Encoding core.Encoding `json:"encoding"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type Keys []Key
|
||||
|
||||
func (Keys) Less(a, b Key) { return a.Name < b.Name }
|
||||
func (k Keys) Sort() { slices.SortStableFunc(k, k.Less) }
|
||||
|
||||
func NewKeys(keys core.Keys) (k Keys) {
|
||||
k = Keys(keys)
|
||||
k.Sort()
|
||||
}
|
||||
|
||||
type Approval struct {
|
||||
ID string `json:"id"`
|
||||
AID string `json:"aid"`
|
||||
Name string `json:"name"`
|
||||
Created int64 `json:"created"`
|
||||
Status types.Status `json:"status"`
|
||||
}
|
||||
|
||||
type Approvals []Approval
|
||||
|
||||
func (Approvals) Less(a, b Approval) { return a.Name < b.Name }
|
||||
func (a Approvals) Sort() { slices.SortStableFunc(a, a.Less) }
|
||||
|
||||
func NewApprovals(m types.ApprovalMap) (a Approvals) {
|
||||
for k, v := range m {
|
||||
a = append(a, Approval{
|
||||
ID: v.ID,
|
||||
AID: k,
|
||||
Name: v.Name,
|
||||
Created: v.Created,
|
||||
Status: v.Status,
|
||||
})
|
||||
}
|
||||
a.Sort()
|
||||
return
|
||||
}
|
||||
136
pkg/api/v1/server/handler.go
Normal file
136
pkg/api/v1/server/handler.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright (C) 2023 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/types"
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/v1/schema"
|
||||
)
|
||||
|
||||
func local(h types.CtxHandler) types.CtxHandler {
|
||||
return func(ctx *types.Context) {
|
||||
addr, err := netip.ParseAddr(ctx.ClientIP())
|
||||
if err != nil {
|
||||
ctx.Err(types.ErrForbidden)
|
||||
return
|
||||
}
|
||||
if !addr.IsLoopback() {
|
||||
ctx.Err(types.ErrForbidden)
|
||||
return
|
||||
}
|
||||
h(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func healthzHandler(ctx *types.Context) {
|
||||
ctx.OK()
|
||||
}
|
||||
|
||||
func newHandler(ctx *types.Context) {
|
||||
if ctx.Method() == "POST" {
|
||||
b, err := ctx.ReadBody()
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error reading request body")
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
var newKey schema.NewKey
|
||||
err = json.Unmarshal(b, &newKey)
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error decoding request body")
|
||||
ctx.Err(types.ErrInvalid)
|
||||
return
|
||||
}
|
||||
err = newKey.Validate()
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error empty name for new key")
|
||||
ctx.Err(types.ErrInvalid)
|
||||
return
|
||||
}
|
||||
key, err := ctx.Data.DB.CreateKey(newKey.Name, newKey.Size, newKey.Encoding)
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error writing loadbalancer", "cluster", cl, "name", n)
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
ctx.JSON(schema.Key(key))
|
||||
}
|
||||
}
|
||||
|
||||
func keyHandler(ctx *types.Context) {
|
||||
id := ctx.Var("id")
|
||||
k, err := ctx.Data.DB.GetKey(id)
|
||||
if err != nil {
|
||||
//TODO error 404
|
||||
ctx.Log.Error(err, "error reading key", "id", id)
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
aid := ctx.Var("aid")
|
||||
if aid == "" {
|
||||
aid, err = ctx.Data.Approvals.New(id, k.Name)
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error creating approval", "id", id, "name", k.Name)
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(schema.ApproveURL(id, aid), http.StatusFound)
|
||||
return
|
||||
}
|
||||
if !ctx.Data.Approvals.Approved(id, aid) {
|
||||
ctx.Redirect(schema.ApproveURL(id, aid), http.StatusFound)
|
||||
return
|
||||
}
|
||||
k, err := ctx.Data.DB.GetKeyWithSecret(id)
|
||||
if err != nil {
|
||||
//TODO error 404
|
||||
ctx.Log.Error(err, "error reading key", "id", id)
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
ctx.JSON(schema.Key(k))
|
||||
case "DELETE":
|
||||
err = ctx.Data.DB.DeleteKey(id)
|
||||
if err != nil {
|
||||
ctx.Log.Error(err, "error deleting key", "id", id, "name", k.Name)
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
ctx.OK()
|
||||
}
|
||||
}
|
||||
|
||||
func reqListHandler(ctx *types.Context) {
|
||||
l := ctx.Data.Approvals.List()
|
||||
ctx.JSON(schema.NewApprovals(l))
|
||||
}
|
||||
|
||||
func keyListHandler(ctx *types.Context) {
|
||||
k, err := ctx.Data.DB.GetKeys()
|
||||
if err != nil {
|
||||
//TODO error 404
|
||||
ctx.Log.Error(err, "error reading keys")
|
||||
ctx.Err(types.ErrISE)
|
||||
return
|
||||
}
|
||||
ctx.JSON(schema.NewKeys(k))
|
||||
}
|
||||
|
||||
func reqHandler(ctx *types.Context) {
|
||||
id := ctx.Var("id")
|
||||
status := types.Pending
|
||||
switch ctx.Method() {
|
||||
case "PUT":
|
||||
status = types.Approved
|
||||
case "DELETE":
|
||||
status = types.Rejected
|
||||
}
|
||||
ctx.Data.Approvals.Update(id, status)
|
||||
ctx.OK()
|
||||
}
|
||||
59
pkg/api/v1/server/routes.go
Normal file
59
pkg/api/v1/server/routes.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (C) 2023 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/types"
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/api/v1/schema"
|
||||
"git.giftfish.de/ston1th/keyctl/pkg/core"
|
||||
)
|
||||
|
||||
const (
|
||||
healthz = "/healthz"
|
||||
keyID = schema.KeyPath + core.IDRoute
|
||||
keyReqID = schema.KeyPath + core.IDRoute + core.AIDRoute
|
||||
reqID = schema.ReqPath + core.IDRoute
|
||||
)
|
||||
|
||||
var Routes = []types.Route{
|
||||
{
|
||||
healthz,
|
||||
healthzHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
schema.KeyPath,
|
||||
local(keyListHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
schema.KeyPath,
|
||||
local(newHandler),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
keyID,
|
||||
keyHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
keyReqID,
|
||||
keyHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
keyID,
|
||||
local(keyHandler),
|
||||
[]string{"DELETE"},
|
||||
},
|
||||
{
|
||||
schema.ReqPath,
|
||||
local(reqListHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
reqID,
|
||||
local(reqHandler),
|
||||
[]string{"PUT", "DELETE"},
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue