Skip to content

Commit

Permalink
added force apply and improved managed fields
Browse files Browse the repository at this point in the history
managedfields:
- added more tests
- moved to internal
- use FieldsV1 for merge and not rely on "id" field
  • Loading branch information
jlarfors committed Feb 28, 2024
1 parent 7ac8f6c commit fa54be9
Show file tree
Hide file tree
Showing 22 changed files with 823 additions and 526 deletions.
4 changes: 1 addition & 3 deletions pkg/gateway/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package gateway
import (
"bytes"
"encoding/json"
"fmt"
"net/http"

"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -65,7 +64,7 @@ func (o *ObjectHandler) get(w http.ResponseWriter, r *http.Request) {
}

func (o *ObjectHandler) apply(w http.ResponseWriter, r *http.Request) {
manager := r.Header.Get(hz.HeaderFieldManager)
manager := r.Header.Get(hz.HeaderApplyFieldManager)
client := hz.NewClient(
o.Conn,
hz.WithClientSessionFromRequest(r),
Expand All @@ -80,7 +79,6 @@ func (o *ObjectHandler) apply(w http.ResponseWriter, r *http.Request) {
)
return
}
fmt.Println("GENERIC OBJECT: ", obj)
if err := client.Apply(r.Context(), hz.WithApplyObject(obj)); err != nil {
httpError(w, err)
return
Expand Down
24 changes: 15 additions & 9 deletions pkg/hz/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ const (
)

const (
HeaderStatus = "Hz-Status"
HeaderAuthorization = "Hz-Authorization"
HeaderFieldManager = "Hz-Field-Manager"
HeaderStatus = "Hz-Status"
HeaderAuthorization = "Hz-Authorization"
HeaderApplyFieldManager = "Hz-Apply-Field-Manager"
HeaderApplyForceConflicts = "Hz-Apply-Force-Conflicts"
)

const (
Expand Down Expand Up @@ -269,7 +270,6 @@ func SessionFromRequest(req *http.Request) string {
if sessionCookie, err := req.Cookie(CookieSession); err == nil {
return sessionCookie.Value
}
fmt.Println("REQ HEADER:", req.Header.Get(HeaderAuthorization))
return req.Header.Get(HeaderAuthorization)
}

Expand Down Expand Up @@ -476,6 +476,7 @@ type applyOptions struct {
object Objecter
data []byte
objectKey ObjectKeyer
force bool
}

func WithApplyObject(object Objecter) ApplyOption {
Expand All @@ -497,6 +498,12 @@ func WithApplyKey(key ObjectKeyer) ApplyOption {
}
}

func WithApplyForce(force bool) ApplyOption {
return func(ao *applyOptions) {
ao.force = force
}
}

func (c Client) Apply(
ctx context.Context,
opts ...ApplyOption,
Expand Down Expand Up @@ -526,27 +533,26 @@ func (c Client) Apply(
if err != nil {
return fmt.Errorf("marshalling object: %w", err)
}
key, err = keyFromObjectStrict(ao.object)
key, err = KeyFromObjectConcrete(ao.object)
if err != nil {
return fmt.Errorf("invalid object: %w", err)
}
}
if ao.objectKey != nil {
var err error
key, err = keyFromObjectStrict(ao.objectKey)
key, err = KeyFromObjectConcrete(ao.objectKey)
if err != nil {
return fmt.Errorf("invalid object: %w", err)
}
}
fmt.Println("key:", key)

msg := nats.NewMsg(
c.SubjectPrefix() + fmt.Sprintf(
SubjectStoreApply,
key,
),
)
msg.Header.Set(HeaderFieldManager, c.Manager)
msg.Header.Set(HeaderApplyFieldManager, c.Manager)
msg.Header.Set(HeaderApplyForceConflicts, strconv.FormatBool(ao.force))
msg.Header.Set(HeaderAuthorization, c.Session)
msg.Data = ao.data
ctx, cancel := context.WithTimeout(ctx, time.Second)
Expand Down
27 changes: 27 additions & 0 deletions pkg/hz/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ func ErrorFromHTTP(resp *http.Response) error {
}
}

// ErrorWrap takes an error and checks if it is an [Error].
// If it is, it will make a copy of the [Error], add the given message and
// return it. The status will remain the same.
//
// If it is not an [Error], it will wrap the given error in an [Error] with the
// given status and message.
func ErrorWrap(
err error,
status int,
message string,
) error {
if err == nil {
return nil
}
var hErr *Error
if errors.As(err, &hErr) {
return &Error{
Status: hErr.Status,
Message: fmt.Sprintf("%s: %s", message, hErr.Message),
}
}
return &Error{
Status: status,
Message: fmt.Sprintf("%s: %s", message, err.Error()),
}
}

// respondError responds to a NATS message with an error.
// It expects the err to be an *Error and will use the status and message for
// the response.
Expand Down
2 changes: 1 addition & 1 deletion pkg/hz/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (c *HTTPClient) Apply(ctx context.Context, opts ...HTTPApplyOption) error {

req.Header.Add("Content-Type", "application/json")
req.Header.Add(HeaderAuthorization, c.Session)
req.Header.Add(HeaderFieldManager, c.Manager)
req.Header.Add(HeaderApplyFieldManager, c.Manager)

resp, err := http.DefaultClient.Do(req)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/hz/managedfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"

"github.com/verifa/horizon/pkg/managedfields"
"github.com/verifa/horizon/pkg/internal/managedfields"
)

// ExtractManagedFields creates an object containing the fields managed by the
Expand Down
2 changes: 1 addition & 1 deletion pkg/hz/managedfields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/verifa/horizon/pkg/hz"
"github.com/verifa/horizon/pkg/managedfields"
"github.com/verifa/horizon/pkg/internal/managedfields"
tu "github.com/verifa/horizon/pkg/testutil"
)

Expand Down
16 changes: 14 additions & 2 deletions pkg/hz/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
"time"

"github.com/verifa/horizon/pkg/managedfields"
"github.com/verifa/horizon/pkg/internal/managedfields"
)

type Objecter interface {
Expand All @@ -28,6 +28,14 @@ type ObjectKeyer interface {
ObjectGroup() string
}

// KeyFromObject takes an ObjectKeyer and returns a string key.
// Any empty fields in the ObjectKeyer are replaced with "*" which works well
// for nats subjects to list objects.
//
// If performing an action on a specific object (e.g. get, create, apply) the
// key cannot contain "*".
// In this case you can use [KeyFromObjectConcrete] which makes sure the
// ObjectKeyer is concrete.
func KeyFromObject(obj ObjectKeyer) string {
account := "*"
if obj.ObjectAccount() != "" {
Expand All @@ -54,7 +62,11 @@ func KeyFromObject(obj ObjectKeyer) string {
)
}

func keyFromObjectStrict(obj ObjectKeyer) (string, error) {
// KeyFromObjectConcrete takes an ObjectKeyer and returns a string key.
// It returns an error if any of the fields are empty.
// This is useful when you want to ensure the key is concrete when performing
// operations on specific objects (e.g. get, create, apply).
func KeyFromObjectConcrete(obj ObjectKeyer) (string, error) {
var errs error
if obj.ObjectAccount() == "" {
errs = errors.Join(errs, fmt.Errorf("account is required"))
Expand Down
1 change: 0 additions & 1 deletion pkg/hzctl/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ func Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
// Add return_url to login request.
form.Add("return_url", returnURL)
loginURL.RawQuery = form.Encode()
fmt.Println("loginURL: ", loginURL.String())

if err := openBrowser(loginURL.String()); err != nil {
return nil, fmt.Errorf("opening browser: %w", err)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,58 @@ func (m ManagedFields) FieldManager(manager string) (FieldManager, bool) {
return FieldManager{}, false
}

type FieldsType string

const (
FieldsTypeV1 FieldsType = "FieldsV1"
)

// FieldManager is a manager of fields for a given object.
// An object can have multiple field managers, and those field managers make up
// the managed fields for the object.
type FieldManager struct {
// Manager is the unique name of the manager.
Manager string `json:"manager" cue:"=~\"^[a-zA-Z0-9-_]+$\""`
// Operation Operation `json:"operation" cue:"=~\"^[a-zA-Z0-9-_]+$\""`
// Time time.Time `json:"time" cue:",opt"`
FieldsType string `json:"fieldsType" cue:"=~\"^[a-zA-Z0-9-_]+$\""`
FieldsV1 FieldsV1 `json:"fieldsV1"`
// FieldsType is the type of fields that are managed.
// Only supported type is right now is "FieldsV1".
FieldsType FieldsType `json:"fieldsType" cue:"=~\"^[a-zA-Z0-9-_]+$\""`
// FieldsV1 is the actual fields that are managed.
FieldsV1 FieldsV1 `json:"fieldsV1"`
}

// FieldsV1 is the actual fields that are managed.
type FieldsV1 struct {
// Parent is a pointer to the parent field.
// It is only used when creating and operating on the managed fields, and
// not stored together with the object.
Parent *FieldsV1Step `json:"-"`
// Fields = Object
// Fields represents an object, and all of its fields.
Fields map[FieldsV1Key]FieldsV1 `json:"-"`
// Elements = Array
// Elements represents an array.
// To allow managing indexes of an array, we use a key (not a numerical
// index) for the array index.
//
// The fancy term for this is "associative list".
Elements map[FieldsV1Key]FieldsV1 `json:"-"`
}

// IsLeaf returns true if the field is a leaf node and does not have any fields
// (object) or elements (array).
func (f FieldsV1) IsLeaf() bool {
return len(f.Fields) == 0 && len(f.Elements) == 0
}

// Path constructs a path from this node to the root.
// It only works if the parent is set, which is only the case when creating a
// [FieldsV1].
func (f FieldsV1) Path() FieldsV1Path {
if f.Parent == nil {
return FieldsV1Path{}
}
return append(f.Parent.Field.Path(), *f.Parent)
}

// FieldsV1Path is a series of steps from a node (root) to a leaf node.
type FieldsV1Path []FieldsV1Step

func (p FieldsV1Path) String() string {
Expand All @@ -73,6 +98,8 @@ func (s FieldsV1Step) String() string {
return strings.Join(steps, ".")
}

// FieldsV1Key represents a key in a FieldsV1 object.
// It can be either an object key (string) or an array (key-value).
type FieldsV1Key struct {
Type FieldsV1KeyType `json:"-"`
Key string `json:"-"`
Expand Down Expand Up @@ -214,11 +241,3 @@ func (f FieldsV1Key) String() string {
}
return fmt.Sprintf("{%s:%s}", f.Key, f.Value)
}

type Operation string

const (
OperationCreate Operation = "Create"
OperationUpdate Operation = "Update"
OperationApply Operation = "Apply"
)
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit fa54be9

Please sign in to comment.