Skip to content

Commit

Permalink
feat(ui): service tags
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephKav committed Jan 21, 2025
1 parent fa0fcf9 commit afd8997
Show file tree
Hide file tree
Showing 54 changed files with 2,924 additions and 844 deletions.
2 changes: 1 addition & 1 deletion config/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func testServiceURL(id string) *service.Service {
LatestVersion: lv,
DeployedVersionLookup: dv,
Dashboard: *service.NewDashboardOptions(
test.BoolPtr(false), "test", "https://release-argus.io", "https://release-argus.io/docs",
test.BoolPtr(false), "test", "https://release-argus.io", "https://release-argus.io/docs", nil,
&service.DashboardOptionsDefaults{}, &service.DashboardOptionsDefaults{}),
Options: *options,
Status: *status.New(
Expand Down
92 changes: 89 additions & 3 deletions service/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
package service

import (
"encoding/json"
"errors"
"fmt"

"github.com/release-argus/Argus/util"
"gopkg.in/yaml.v3"
)

// DashboardOptionsBase are the base options for the Dashboard.
Expand All @@ -44,9 +47,10 @@ func NewDashboardOptionsDefaults(
type DashboardOptions struct {
DashboardOptionsBase `yaml:",inline" json:",inline"`

Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` // Icon URL to use for messages/Web UI.
IconLinkTo string `yaml:"icon_link_to,omitempty" json:"icon_link_to,omitempty"` // URL to redirect Icon clicks to.
WebURL string `yaml:"web_url,omitempty" json:"web_url,omitempty"` // URL to provide on the Web UI.
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` // Icon URL to use for messages/Web UI.
IconLinkTo string `yaml:"icon_link_to,omitempty" json:"icon_link_to,omitempty"` // URL to redirect Icon clicks to.
WebURL string `yaml:"web_url,omitempty" json:"web_url,omitempty"` // URL to provide on the Web UI.
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` // Tags for the Service.

Defaults *DashboardOptionsDefaults `yaml:"-" json:"-"` // Defaults.
HardDefaults *DashboardOptionsDefaults `yaml:"-" json:"-"` // Hard defaults.
Expand All @@ -58,6 +62,7 @@ func NewDashboardOptions(
icon string,
iconLinkTo string,
webURL string,
tags []string,
defaults, hardDefaults *DashboardOptionsDefaults,
) *DashboardOptions {
return &DashboardOptions{
Expand All @@ -66,10 +71,91 @@ func NewDashboardOptions(
Icon: icon,
IconLinkTo: iconLinkTo,
WebURL: webURL,
Tags: tags,
Defaults: defaults,
HardDefaults: hardDefaults}
}

// UnmarshalJSON handles the unmarshalling of a DashboardOptions.
func (d *DashboardOptions) UnmarshalJSON(data []byte) error {
aux := &struct {
*DashboardOptionsBase `json:",inline"`

Icon *string `json:"icon,omitempty"` // Icon URL to use for messages/Web UI.
IconLinkTo *string `json:"icon_link_to,omitempty"` // URL to redirect Icon clicks to.
WebURL *string `json:"web_url,omitempty"` // URL to provide on the Web UI.
Tags json.RawMessage `json:"tags,omitempty"` // Tags for the Service.
}{
DashboardOptionsBase: &d.DashboardOptionsBase,
Icon: &d.Icon,
IconLinkTo: &d.IconLinkTo,
WebURL: &d.WebURL,
}

// Unmarshal into aux.
if err := json.Unmarshal(data, &aux); err != nil {
return fmt.Errorf("failed to unmarshal DashboardOptions:\n%w", err)
}

// Tags
if len(aux.Tags) > 0 {
var tagsAsString string
var tagsAsArray []string

// Try to unmarshal as a list of strings
if err := json.Unmarshal(aux.Tags, &tagsAsArray); err == nil {
d.Tags = tagsAsArray
// Try to unmarshal as a single string
} else if err := json.Unmarshal(aux.Tags, &tagsAsString); err == nil {
d.Tags = []string{tagsAsString}
} else {
return errors.New("error in tags field:\ntype: <invalid> (expected string or list of strings)")
}
}

return nil
}

// UnmarshalYAML handles the unmarshalling of a DashboardOptions.
func (d *DashboardOptions) UnmarshalYAML(value *yaml.Node) error {
aux := &struct {
*DashboardOptionsBase `yaml:",inline"`

Icon *string `yaml:"icon,omitempty"` // Icon URL to use for messages/Web UI.
IconLinkTo *string `yaml:"icon_link_to,omitempty"` // URL to redirect Icon clicks to.
WebURL *string `yaml:"web_url,omitempty"` // URL to provide on the Web UI.
Tags util.RawNode `yaml:"tags,omitempty"` // Tags for the Service.
}{
DashboardOptionsBase: &d.DashboardOptionsBase,
Icon: &d.Icon,
IconLinkTo: &d.IconLinkTo,
WebURL: &d.WebURL,
}

// Unmarshal into aux.
if err := value.Decode(&aux); err != nil {
return fmt.Errorf("failed to unmarshal DashboardOptions:\n%w", err)
}

// Tags
if aux.Tags.Node != nil {
var tagsAsString string
var tagsAsArray []string

// Try to unmarshal as a list of strings
if err := aux.Tags.Decode(&tagsAsArray); err == nil {
d.Tags = tagsAsArray
// Try to unmarshal as a single string
} else if err := aux.Tags.Decode(&tagsAsString); err == nil {
d.Tags = []string{tagsAsString}
} else {
return errors.New("error in tags field:\ntype: <invalid> (expected string or list of strings)")
}
}

return nil
}

// GetAutoApprove returns whether new releases are auto-approved.
func (d *DashboardOptions) GetAutoApprove() bool {
return *util.FirstNonDefault(
Expand Down
215 changes: 215 additions & 0 deletions service/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,223 @@ import (

"github.com/release-argus/Argus/test"
"github.com/release-argus/Argus/util"
"gopkg.in/yaml.v3"
)

func TestNewDashboardOptions(t *testing.T) {
// GIVEN a set of input values
tests := map[string]struct {
autoApprove *bool
icon string
iconLinkTo string
webURL string
tags []string
defaults *DashboardOptionsDefaults
hardDefaults *DashboardOptionsDefaults
want *DashboardOptions
}{
"all fields set": {
autoApprove: test.BoolPtr(true),
icon: "icon-url",
iconLinkTo: "icon-link",
webURL: "web-url",
tags: []string{"tag1", "tag2"},
defaults: &DashboardOptionsDefaults{DashboardOptionsBase: DashboardOptionsBase{AutoApprove: test.BoolPtr(false)}},
hardDefaults: &DashboardOptionsDefaults{DashboardOptionsBase: DashboardOptionsBase{AutoApprove: test.BoolPtr(false)}},
want: &DashboardOptions{
DashboardOptionsBase: DashboardOptionsBase{AutoApprove: test.BoolPtr(true)},
Icon: "icon-url",
IconLinkTo: "icon-link",
WebURL: "web-url",
Tags: []string{"tag1", "tag2"},
Defaults: &DashboardOptionsDefaults{DashboardOptionsBase: DashboardOptionsBase{AutoApprove: test.BoolPtr(false)}},
HardDefaults: &DashboardOptionsDefaults{DashboardOptionsBase: DashboardOptionsBase{AutoApprove: test.BoolPtr(false)}},
},
},
"defaults": {
autoApprove: nil,
icon: "",
iconLinkTo: "",
webURL: "",
tags: nil,
defaults: nil,
hardDefaults: nil,
want: &DashboardOptions{
DashboardOptionsBase: DashboardOptionsBase{AutoApprove: nil},
Icon: "",
IconLinkTo: "",
WebURL: "",
Tags: nil,
Defaults: nil,
HardDefaults: nil,
},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

// WHEN NewDashboardOptions is called with them.
got := NewDashboardOptions(tc.autoApprove, tc.icon, tc.iconLinkTo, tc.webURL, tc.tags, tc.defaults, tc.hardDefaults)

// THEN the result is as expected.
gotStr := util.ToJSONString(got)
wantStr := util.ToJSONString(tc.want)
if gotStr != wantStr {
t.Errorf("NewDashboardOptions() result mismatch\n%q\ngot:\n%v",
wantStr, gotStr)
}
})
}
}

func TestDashboardOptions_UnmarshalJSON(t *testing.T) {
// GIVEN a JSON string that represents a DashboardOptions.
tests := map[string]struct {
jsonData string
errRegex string
want *DashboardOptions
}{
"invalid json": {
jsonData: `{invalid: json}`,
errRegex: test.TrimYAML(`
failed to unmarshal DashboardOptions:
invalid character.*$`),
want: &DashboardOptions{},
},
"tags - []string": {
jsonData: `{
"tags": [
"foo",
"bar"
]
}`,
errRegex: `^$`,
want: &DashboardOptions{
Tags: []string{"foo", "bar"},
},
},
"tags - string": {
jsonData: `{
"tags": "foo"
}`,
errRegex: `^$`,
want: &DashboardOptions{
Tags: []string{"foo"},
},
},
"tags - invalid": {
jsonData: `{
"tags": {
"foo": "bar"
}
}`,
errRegex: test.TrimYAML(`
^error in tags field:
type: <invalid>.*$`),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

// Default to an empty DashboardOptions.
dashboardOptions := &DashboardOptions{}

// WHEN the JSON is unmarshalled into a DashboardOptions.
err := dashboardOptions.UnmarshalJSON([]byte(test.TrimJSON(tc.jsonData)))

// THEN the error is as expected.
e := util.ErrorToString(err)
if !util.RegexCheck(tc.errRegex, e) {
t.Errorf("DashboardOptions.UnmarshalJSON() error mismatch\nwant: %q\ngot: %q",
tc.errRegex, e)
}
// AND the result is as expected.
gotString := util.ToJSONString(dashboardOptions)
wantString := util.ToJSONString(tc.want)
if tc.want != nil && gotString != wantString {
t.Errorf("DashboardOptions.UnmarshalJSON() result mismatch\n%q\ngot:\n%q",
wantString, gotString)
}
})
}
}

func TestDashboardOptions_UnmarshalYAML(t *testing.T) {
tests := map[string]struct {
yamlData string
errRegex string
want *DashboardOptions
}{
"invalid yaml": {
yamlData: `invalid yaml`,
errRegex: test.TrimYAML(`
failed to unmarshal DashboardOptions:
yaml: unmarshal errors:
.*cannot unmarshal.*$`),
want: &DashboardOptions{},
},
"tags - []string": {
yamlData: `
tags:
- foo
- bar
`,
errRegex: `^$`,
want: &DashboardOptions{
Tags: []string{"foo", "bar"},
},
},
"tags - string": {
yamlData: `
tags: foo
`,
errRegex: `^$`,
want: &DashboardOptions{
Tags: []string{"foo"},
},
},
"tags - invalid": {
yamlData: `
tags:
foo: bar
`,
errRegex: test.TrimYAML(`
^error in tags field:
type: <invalid>.*$`),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

// Default to an empty DashboardOptions.
dashboardOptions := &DashboardOptions{}

// WHEN the YAML is unmarshalled into a DashboardOptions
err := yaml.Unmarshal([]byte(test.TrimYAML(tc.yamlData)), &dashboardOptions)

// THEN the error is as expected
e := util.ErrorToString(err)
if !util.RegexCheck(tc.errRegex, e) {
t.Errorf("DashboardOptions.UnmarshalYAML() error mismatch\nwant: %q\ngot: %q",
tc.errRegex, e)
}
// AND the result is as expected
gotStr := util.ToJSONString(dashboardOptions)
wantStr := util.ToJSONString(tc.want)
if tc.want != nil && gotStr != wantStr {
t.Errorf("DashboardOptions.UnmarshalYAML() result mismatch\nwant: %s\ngot: %s",
wantStr, gotStr)
}
})
}
}

func TestDashboardOptions_GetAutoApprove(t *testing.T) {
// GIVEN a DashboardOptions
tests := map[string]struct {
Expand Down
2 changes: 1 addition & 1 deletion service/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func testService(t *testing.T, id string, sType string) *Service {
LatestVersion: testLatestVersion(t, sType, false),
DeployedVersionLookup: testDeployedVersionLookup(t, false),
Dashboard: *NewDashboardOptions(
test.BoolPtr(false), "test", "https://release-argus.io", "https://release-argus.io",
test.BoolPtr(false), "test", "https://release-argus.io", "https://release-argus.io", nil,
&DashboardOptionsDefaults{}, &DashboardOptionsDefaults{}),
Status: *testStatus(),
Options: *testOptions(),
Expand Down
2 changes: 1 addition & 1 deletion service/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (s *Service) Init(

// Notify/
// use defaults?
if s.Notify == nil && len(defaults.Notify) != 0 {
if len(s.Notify) == 0 && len(defaults.Notify) != 0 {
s.Notify = make(shoutrrr.Slice, len(defaults.Notify))
for key := range defaults.Notify {
s.Notify[key] = &shoutrrr.Shoutrrr{}
Expand Down
2 changes: 1 addition & 1 deletion service/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (s *Service) giveSecretsDeployedVersion(oldDeployedVersion *deployedver.Loo
// giveSecretsNotify from the `oldNotifies`.
func (s *Service) giveSecretsNotify(oldNotifies shoutrrr.Slice, secretRefs map[string]oldStringIndex) {
//nolint:typecheck
if s.Notify == nil || oldNotifies == nil ||
if len(s.Notify) == 0 || len(oldNotifies) == 0 ||
len(secretRefs) == 0 {
return
}
Expand Down
Loading

0 comments on commit afd8997

Please sign in to comment.