Skip to content

Commit

Permalink
groups: added implementation with API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
davidallendj committed Oct 24, 2024
1 parent 43c4d93 commit 71092b9
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 4 deletions.
Binary file added cmd/cloud-init-server/cloud-init-server
Binary file not shown.
113 changes: 113 additions & 0 deletions cmd/cloud-init-server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewCiHandler(s ciStore, c *smdclient.SMDClient) *CiHandler {

// Enumeration for cloud-init data categories
type ciDataKind uint

// Takes advantage of implicit repetition and iota's auto-incrementing
const (
UserData ciDataKind = iota
Expand Down Expand Up @@ -213,3 +214,115 @@ func (h CiHandler) DeleteEntry(w http.ResponseWriter, r *http.Request) {

render.JSON(w, r, map[string]string{"status": "success"})
}

func (h CiHandler) AddGroupData(w http.ResponseWriter, r *http.Request) {
// type alias to simplify abstraction
var (
id string = chi.URLParam(r, "id")
body []byte
data memstore.Data
err error
)

// read the POST body for JSON data
body, err = io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// unmarshal data to add to MemStore
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// add a new group to meta-data if it doesn't already exist
err = h.store.AddGroups(id, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

}

func (h CiHandler) GetGroupData(w http.ResponseWriter, r *http.Request) {
var (
id string = chi.URLParam(r, "id")
data memstore.Data
bytes []byte
err error
)

// get group data from MemStore if it exists
data, err = h.store.GetGroups(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// marshal to YAML and print the group data to standard output
bytes, err = yaml.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)

}

// UpdateGroupData expects a request containing POST data as a JSON. The data
// received in the request should ONLY contain the data to be included for a
// "meta-data.groups" and NOT "meta-data". See "AddGroup" in 'ciMemStore.go'
// for an example.
func (h CiHandler) UpdateGroupData(w http.ResponseWriter, r *http.Request) {
// type alias to simplify abstraction
var (
id string = chi.URLParam(r, "id")
body []byte
data memstore.Data
err error
)

// read the POST body for JSON data
body, err = io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// unmarshal data to add to MemStore
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// update groups in meta-data
err = h.store.UpdateGroups(id, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

}

func (h CiHandler) RemoveGroupData(w http.ResponseWriter, r *http.Request) {
var (
id string = chi.URLParam(r, "id")
err error
)

// remove group data with specified name
err = h.store.RemoveGroups(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

func writeInternalError(w http.ResponseWriter, err string) {
http.Error(w, err, http.StatusInternalServerError)
// log.Error().Err(err)
}
19 changes: 17 additions & 2 deletions cmd/cloud-init-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import (
"flag"
"fmt"
"net/http"
"os"
"time"

"github.com/OpenCHAMI/cloud-init/internal/memstore"
"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/jwtauth/v5"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
zlog "github.com/rs/zerolog/log"

openchami_authenticator "github.com/openchami/chi-middleware/auth"
openchami_logger "github.com/openchami/chi-middleware/log"
)

var (
Expand Down Expand Up @@ -43,6 +49,10 @@ func main() {
fmt.Println("No JWKS URL provided; secure route will be disabled")
}

// Setup logger
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
logger := zlog.Output(zerolog.ConsoleWriter{Out: os.Stderr})

// Primary router and shared SMD client
router := chi.NewRouter()
router.Use(
Expand All @@ -51,7 +61,8 @@ func main() {
middleware.Logger,
middleware.Recoverer,
middleware.StripSlashes,
middleware.Timeout(60 * time.Second),
middleware.Timeout(60*time.Second),
openchami_logger.OpenCHAMILogger(logger),
)
sm := smdclient.NewSMDClient(smdEndpoint, tokenEndpoint)

Expand All @@ -69,7 +80,7 @@ func main() {
router_sec := chi.NewRouter()
router_sec.Use(
jwtauth.Verifier(keyset),
jwtauth.Authenticator(keyset),
openchami_authenticator.AuthenticatorWithRequiredClaims(keyset, []string{"sub", "iss", "aud"}),
)
initCiRouter(router_sec, ciHandler_sec)
router.Mount("/cloud-init-secure", router_sec)
Expand All @@ -92,4 +103,8 @@ func initCiRouter(router chi.Router, handler *CiHandler) {
router.Get("/{id}/vendor-data", handler.GetDataByMAC(VendorData))
router.Put("/{id}", handler.UpdateEntry)
router.Delete("/{id}", handler.DeleteEntry)
router.Post("/{id}/groups", handler.AddGroupData)
router.Get("/{id}/groups", handler.GetGroupData)
router.Put("/{id}/groups", handler.UpdateGroupData)
router.Delete("/{id}/groups", handler.RemoveGroupData)
}
7 changes: 7 additions & 0 deletions cmd/cloud-init-server/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import (
)

// ciStore is an interface for storing cloud-init entries

type ciStore interface {
Add(name string, ci citypes.CI) error
Get(name string, sm *smdclient.SMDClient) (citypes.CI, error)
List() (map[string]citypes.CI, error)
Update(name string, ci citypes.CI) error
Remove(name string) error

// metadata group API
AddGroups(name string, groupData map[string]any) error
GetGroups(name string) (map[string]any, error)
UpdateGroups(name string, groupData map[string]any) error
RemoveGroups(name string) error
}
155 changes: 153 additions & 2 deletions internal/memstore/ciMemStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import (
)

var (
NotFoundErr = errors.New("not found")
ExistingEntryErr = errors.New("Data exists for this entry. Update instead")
NotFoundErr = errors.New("Not found.")
ExistingEntryErr = errors.New("Data exists for this entry. Update instead.")
)

type (
Data = map[string]any
GroupData = map[string]Data
)

type MemStore struct {
Expand All @@ -25,9 +30,12 @@ func NewMemStore() *MemStore {
}
}

// Add creates new cloud-init data and stores in MemStore. If the data already
// exists, an error will be returned
func (m MemStore) Add(name string, ci citypes.CI) error {
curr := m.list[name]

// add user data if no current data
if ci.CIData.UserData != nil {
if curr.CIData.UserData == nil {
curr.CIData.UserData = ci.CIData.UserData
Expand Down Expand Up @@ -56,6 +64,7 @@ func (m MemStore) Add(name string, ci citypes.CI) error {
return nil
}

// Get retrieves data stored in MemStore and returns it or an error
func (m MemStore) Get(name string, sm *smdclient.SMDClient) (citypes.CI, error) {

ci_merged := new(citypes.CI)
Expand Down Expand Up @@ -94,6 +103,17 @@ func (m MemStore) Get(name string, sm *smdclient.SMDClient) (citypes.CI, error)
}
}

// Merge combines cloud-init data from MemStore with new citypes.CI
func (m MemStore) Merge(name string, newData citypes.CI) (citypes.CI, error) {
ci := new(citypes.CI)
if v, ok := m.list[name]; ok {
ci.CIData.UserData = lo.Assign(newData.CIData.UserData, v.CIData.UserData)
ci.CIData.VendorData = lo.Assign(newData.CIData.VendorData, v.CIData.VendorData)
ci.CIData.MetaData = lo.Assign(newData.CIData.MetaData, v.CIData.MetaData)
}
return *ci, nil
}

func (m MemStore) List() (map[string]citypes.CI, error) {
return m.list, nil
}
Expand Down Expand Up @@ -121,3 +141,134 @@ func (m MemStore) Remove(name string) error {
delete(m.list, name)
return nil
}

/*
AddGroup adds a new group with it's associated meta-data.
Example:
data := map[string]any{
"x3000": map[string]any {
"syslog_aggregator": "192.168.0.1",
},
"canary-123": map[string]any {
"syslog_aggregator": "127.0.0.1",
}
}
AddGroup("compute", "x3000", data)
{
"name": "compute",
...
"meta-data": {
"groups": { // POST request should only include this data
"x3000": {
"syslog_aggregator": "192.168.0.1"
},
"canary-123": {
"hello": "world"
}
}
}
}
*/
func (m MemStore) AddGroups(name string, newGroupData Data) error {
var (
node citypes.CI
existingGroupData Data
)

// get CI data and add groups to metadata if not exists
node, ok := m.list[name]
if ok {
// check if data already exists, otherwise add
if node.CIData.MetaData != nil {
return ExistingEntryErr
} else {
// check if there's already a 'groups' property
existingGroupData = getGroupsFromMetadata(node)
if existingGroupData != nil {
setGroupsInMetadata(node, newGroupData)
} else {
// groups already exist so return error
return ExistingEntryErr
}
}
} else {
// no node, component, or xname found which is required for groups
return NotFoundErr
}
return nil
}

// GetGroup returns the 'meta-data.groups' found with the provided 'name' argument.
// Returns an error if the data is not found.
func (m MemStore) GetGroups(name string) (Data, error) {
var (
node citypes.CI
groupData Data
)
node, ok := m.list[name]
if ok {
groupData = getGroupsFromMetadata(node)
if groupData != nil {
return groupData, nil
} else {
// no group data found so return error
return nil, NotFoundErr
}
}
// no node, component, or xname found so return error
return nil, NotFoundErr
}

// UpdateGroups updates the data for an existing 'meta-data.groups'. An error is
// returned if it is not found.
func (m MemStore) UpdateGroups(name string, newGroupData Data) error {
var (
node citypes.CI
existingGroupData Data
)

// get CI data and update groups whether it exists or not
node, ok := m.list[name]
if ok {
existingGroupData = getGroupsFromMetadata(node)
if existingGroupData != nil {
setGroupsInMetadata(node, newGroupData)
return nil
}
// no groups found so return not found error
return NotFoundErr
}
// no node, component, or xname found which is required to update
return NotFoundErr
}

func (m MemStore) RemoveGroups(name string) error {
var (
node citypes.CI
removeGroupsInMetadata = func(ci citypes.CI, name string) {
delete(ci.CIData.MetaData, name)
}
)

node, ok := m.list[name]
if ok {
removeGroupsInMetadata(node, name)
}
return NotFoundErr
}

func getGroupsFromMetadata(ci citypes.CI) Data {
_, ok := ci.CIData.MetaData["groups"]
if ok {
return ci.CIData.MetaData["groups"].(Data)
}
return nil
}

func setGroupsInMetadata(ci citypes.CI, data Data) {
ci.CIData.MetaData["groups"] = data
}

0 comments on commit 71092b9

Please sign in to comment.