feat(backend): use cluster name from config

This commit is contained in:
Łukasz Mierzwa
2020-06-12 19:14:17 +01:00
committed by Łukasz Mierzwa
parent e52cc91964
commit 4f58ff2e58
14 changed files with 703 additions and 24 deletions
+36 -9
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"sort"
"strings"
"github.com/gin-gonic/gin"
"vbom.ml/util/sortorder"
@@ -14,8 +15,6 @@ import (
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/slices"
"github.com/prymitive/karma/internal/uri"
log "github.com/sirupsen/logrus"
)
func getFiltersFromQuery(filterStrings []string) ([]filters.FilterT, bool) {
@@ -89,6 +88,23 @@ func countersToLabelStats(counters map[string]map[string]int) models.LabelNameSt
return data
}
func clusterMembersFromConfig(am *alertmanager.Alertmanager) []string {
members := []string{}
upstreams := alertmanager.GetAlertmanagers()
for _, upstream := range upstreams {
if upstream.Cluster == am.Cluster {
members = append(members, upstream.Name)
}
}
return members
}
func clusterMembersFromAPI(am *alertmanager.Alertmanager) []string {
return am.ClusterMemberNames()
}
func getUpstreams() models.AlertmanagerAPISummary {
summary := models.AlertmanagerAPISummary{}
@@ -96,13 +112,24 @@ func getUpstreams() models.AlertmanagerAPISummary {
upstreams := alertmanager.GetAlertmanagers()
for _, upstream := range upstreams {
members := upstream.ClusterMemberNames()
key, err := slices.StringSliceToSHA1(members)
if err != nil {
log.Errorf("slices.StringSliceToSHA1 error: %s", err)
continue
var clusterName string
if upstream.Cluster != "" {
configPeers := clusterMembersFromConfig(upstream)
apiPeers := clusterMembersFromAPI(upstream)
missing, extra := slices.StringSliceDiff(configPeers, apiPeers)
if len(missing) == 0 && len(extra) == 0 {
clusterName = upstream.Cluster
} else {
clusterName = fmt.Sprintf("%s @ %s", strings.Join(members, " | "), upstream.Cluster)
}
} else {
clusterName = strings.Join(members, " | ")
}
if _, found := clusters[key]; !found {
clusters[key] = members
if _, found := clusters[clusterName]; !found {
clusters[clusterName] = members
}
u := models.AlertmanagerAPIStatus{
@@ -114,7 +141,7 @@ func getUpstreams() models.AlertmanagerAPISummary {
CORSCredentials: upstream.CORSCredentials,
Error: upstream.Error(),
Version: upstream.Version(),
Cluster: upstream.ClusterID(),
Cluster: clusterName,
ClusterMembers: members,
}
if !upstream.ProxyRequests {
+1
View File
@@ -68,6 +68,7 @@ func TestAuthHeader(t *testing.T) {
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
am, err := alertmanager.NewAlertmanager(
"cluster",
fmt.Sprintf("testAuthHeader/%s", version),
testCase.alertmanagerURI,
alertmanager.WithRequestTimeout(time.Second*5),
+1
View File
@@ -178,6 +178,7 @@ func setupUpstreams() error {
}
am, err := alertmanager.NewAlertmanager(
s.Cluster,
s.Name,
s.URI,
alertmanager.WithExternalURI(s.ExternalURI),
+5
View File
@@ -105,6 +105,7 @@ func TestProxy(t *testing.T) {
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"cluster",
"dummy",
"http://localhost:9093",
alertmanager.WithRequestTimeout(time.Second*5),
@@ -207,6 +208,7 @@ func TestProxyHeaders(t *testing.T) {
testCase := testCase //scopelint pin
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"cluster",
"dummy",
testCase.alertmanagerURI,
alertmanager.WithRequestTimeout(time.Second*5),
@@ -310,6 +312,7 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"cluster",
"suburi",
testCase.alertmanagerURI,
alertmanager.WithRequestTimeout(time.Second*5),
@@ -505,6 +508,7 @@ func TestProxyUserRewrite(t *testing.T) {
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"cluster",
"proxyAuth",
"http://localhost",
alertmanager.WithRequestTimeout(time.Second*5),
@@ -997,6 +1001,7 @@ func TestProxySilenceACL(t *testing.T) {
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"cluster",
"proxyACL",
"http://localhost",
alertmanager.WithRequestTimeout(time.Second*5),
@@ -85,7 +85,8 @@ level=info msg=" silences: \"\""
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"
level=info msg=" - name: ro"
level=info msg=" - cluster: \"\""
level=info msg=" name: ro"
level=info msg=" uri: http://localhost:9093"
level=info msg=" external_uri: http://localhost:9093"
level=info msg=" timeout: 10s"
@@ -20,11 +20,13 @@ authorization:
alertmanager:
interval: 10s
servers:
- name: ha1
- cluster: HA
name: ha1
uri: "http://localhost:9093"
timeout: 10s
proxy: true
- name: ha2
- cluster: HA
name: ha2
uri: "http://localhost:9094"
timeout: 10s
readonly: true
@@ -266,7 +268,8 @@ level=info msg=" silences: \"\""
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"
level=info msg=" - name: ha1"
level=info msg=" - cluster: HA"
level=info msg=" name: ha1"
level=info msg=" uri: http://localhost:9093"
level=info msg=" external_uri: \"\""
level=info msg=" timeout: 10s"
@@ -280,7 +283,8 @@ level=info msg=" insecureSkipVerify: false"
level=info msg=" headers: {}"
level=info msg=" cors:"
level=info msg=" credentials: include"
level=info msg=" - name: ha2"
level=info msg=" - cluster: HA"
level=info msg=" name: ha2"
level=info msg=" uri: http://localhost:9094"
level=info msg=" external_uri: \"\""
level=info msg=" timeout: 10s"
@@ -294,7 +298,8 @@ level=info msg=" insecureSkipVerify: false"
level=info msg=" headers: {}"
level=info msg=" cors:"
level=info msg=" credentials: omit"
level=info msg=" - name: local"
level=info msg=" - cluster: \"\""
level=info msg=" name: local"
level=info msg=" uri: http://localhost:9095"
level=info msg=" external_uri: \"\""
level=info msg=" timeout: 40s"
@@ -309,7 +314,8 @@ level=info msg=" headers:"
level=info msg=" X-Auth-Test: some-token-or-other-string"
level=info msg=" cors:"
level=info msg=" credentials: same-origin"
level=info msg=" - name: client-auth"
level=info msg=" - cluster: \"\""
level=info msg=" name: client-auth"
level=info msg=" uri: https://localhost:9096"
level=info msg=" external_uri: \"\""
level=info msg=" timeout: 10s"
+553 -4
View File
@@ -1025,12 +1025,558 @@ func TestUpstreamStatus(t *testing.T) {
CORSCredentials: "include",
Error: `^unknown error \(status 404\): .+`,
Version: "",
Cluster: "843c4a11660fe38ea61e6960a29d4f4796da6488",
Cluster: "default",
ClusterMembers: []string{"default"},
},
},
Clusters: map[string][]string{
"843c4a11660fe38ea61e6960a29d4f4796da6488": {"default"},
"default": {"default"},
},
},
},
{
Name: "HA Cluster",
mocks: []mockT{
{
uri: "http://ha1.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.20.0"} 1`,
},
{
uri: "http://ha2.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.19.0"} 1`,
},
{
uri: "http://ha1.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.20.0"
}
}`,
},
{
uri: "http://ha2.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.19.0"
}
}`,
},
{
uri: "http://ha1.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha1.example.com/api/v2/silences",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/silences",
code: 200,
body: "[]",
},
},
upstreams: []config.AlertmanagerConfig{
{
Cluster: "HA",
Name: "ha1",
URI: "http://ha1.example.com",
Proxy: false,
ReadOnly: false,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
{
Cluster: "HA",
Name: "ha2",
URI: "http://ha2.example.com",
Proxy: false,
ReadOnly: true,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
},
status: models.AlertmanagerAPISummary{
Counters: models.AlertmanagerAPICounters{
Total: 2,
Healthy: 2,
Failed: 0,
},
Instances: []models.AlertmanagerAPIStatus{
{
Name: "ha1",
URI: "http://ha1.example.com",
PublicURI: "http://ha1.example.com",
ReadOnly: false,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.20.0",
Cluster: "HA",
ClusterMembers: []string{"ha1", "ha2"},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
PublicURI: "http://ha2.example.com",
ReadOnly: true,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.19.0",
Cluster: "HA",
ClusterMembers: []string{"ha1", "ha2"},
},
},
Clusters: map[string][]string{
"HA": {"ha1", "ha2"},
},
},
},
{
Name: "HA Cluster Without Name",
mocks: []mockT{
{
uri: "http://ha1.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.20.0"} 1`,
},
{
uri: "http://ha2.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.19.0"} 1`,
},
{
uri: "http://ha1.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.20.0"
}
}`,
},
{
uri: "http://ha2.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.19.0"
}
}`,
},
{
uri: "http://ha1.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha1.example.com/api/v2/silences",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/silences",
code: 200,
body: "[]",
},
},
upstreams: []config.AlertmanagerConfig{
{
Name: "ha1",
URI: "http://ha1.example.com",
Proxy: false,
ReadOnly: false,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "same-site",
},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
Proxy: false,
ReadOnly: true,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "same-site",
},
},
},
status: models.AlertmanagerAPISummary{
Counters: models.AlertmanagerAPICounters{
Total: 2,
Healthy: 2,
Failed: 0,
},
Instances: []models.AlertmanagerAPIStatus{
{
Name: "ha1",
URI: "http://ha1.example.com",
PublicURI: "http://ha1.example.com",
ReadOnly: false,
Headers: map[string]string{},
CORSCredentials: "same-site",
Error: "",
Version: "0.20.0",
Cluster: "ha1 | ha2",
ClusterMembers: []string{"ha1", "ha2"},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
PublicURI: "http://ha2.example.com",
ReadOnly: true,
Headers: map[string]string{},
CORSCredentials: "same-site",
Error: "",
Version: "0.19.0",
Cluster: "ha1 | ha2",
ClusterMembers: []string{"ha1", "ha2"},
},
},
Clusters: map[string][]string{
"ha1 | ha2": {"ha1", "ha2"},
},
},
},
{
Name: "Broken Cluster",
mocks: []mockT{
{
uri: "http://ha1.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.20.0"} 1`,
},
{
uri: "http://ha2.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.19.0"} 1`,
},
{
uri: "http://ha1.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.20.0"
}
}`,
},
{
uri: "http://ha2.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB",
"peers": [
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.19.0"
}
}`,
},
{
uri: "http://ha1.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha1.example.com/api/v2/silences",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/silences",
code: 200,
body: "[]",
},
},
upstreams: []config.AlertmanagerConfig{
{
Cluster: "Broken HA",
Name: "ha1",
URI: "http://ha1.example.com",
Proxy: false,
ReadOnly: false,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
{
Cluster: "Broken HA",
Name: "ha2",
URI: "http://ha2.example.com",
Proxy: false,
ReadOnly: true,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
},
status: models.AlertmanagerAPISummary{
Counters: models.AlertmanagerAPICounters{
Total: 2,
Healthy: 2,
Failed: 0,
},
Instances: []models.AlertmanagerAPIStatus{
{
Name: "ha1",
URI: "http://ha1.example.com",
PublicURI: "http://ha1.example.com",
ReadOnly: false,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.20.0",
Cluster: "ha1 @ Broken HA",
ClusterMembers: []string{"ha1"},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
PublicURI: "http://ha2.example.com",
ReadOnly: true,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.19.0",
Cluster: "ha2 @ Broken HA",
ClusterMembers: []string{"ha2"},
},
},
Clusters: map[string][]string{
"ha1 @ Broken HA": {"ha1"},
"ha2 @ Broken HA": {"ha2"},
},
},
},
{
Name: "Broken Cluster Without Name",
mocks: []mockT{
{
uri: "http://ha1.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.20.0"} 1`,
},
{
uri: "http://ha2.example.com/metrics",
code: 200,
body: `alertmanager_build_info{version="0.19.0"} 1`,
},
{
uri: "http://ha1.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA",
"peers": [
{
"address": "10.16.0.1:9094",
"name": "AAAAAAAAAAAAAAAAAAAAAAAAAA"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.20.0"
}
}`,
},
{
uri: "http://ha2.example.com/api/v2/status",
code: 200,
body: `{
"cluster": {
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB",
"peers": [
{
"address": "10.16.0.2:9094",
"name": "BBBBBBBBBBBBBBBBBBBBBBBBBB"
}
],
"status": "ready"
},
"versionInfo": {
"version":"0.19.0"
}
}`,
},
{
uri: "http://ha1.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha1.example.com/api/v2/silences",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/alerts/groups",
code: 200,
body: "[]",
},
{
uri: "http://ha2.example.com/api/v2/silences",
code: 200,
body: "[]",
},
},
upstreams: []config.AlertmanagerConfig{
{
Name: "ha1",
URI: "http://ha1.example.com",
Proxy: false,
ReadOnly: false,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
Proxy: false,
ReadOnly: true,
Headers: map[string]string{},
CORS: config.AlertmanagerCORS{
Credentials: "omit",
},
},
},
status: models.AlertmanagerAPISummary{
Counters: models.AlertmanagerAPICounters{
Total: 2,
Healthy: 2,
Failed: 0,
},
Instances: []models.AlertmanagerAPIStatus{
{
Name: "ha1",
URI: "http://ha1.example.com",
PublicURI: "http://ha1.example.com",
ReadOnly: false,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.20.0",
Cluster: "ha1",
ClusterMembers: []string{"ha1"},
},
{
Name: "ha2",
URI: "http://ha2.example.com",
PublicURI: "http://ha2.example.com",
ReadOnly: true,
Headers: map[string]string{},
CORSCredentials: "omit",
Error: "",
Version: "0.19.0",
Cluster: "ha2",
ClusterMembers: []string{"ha2"},
},
},
Clusters: map[string][]string{
"ha1": {"ha1"},
"ha2": {"ha2"},
},
},
},
@@ -1047,7 +1593,10 @@ func TestUpstreamStatus(t *testing.T) {
alertmanager.UnregisterAll()
mockConfig()
config.Config.Alertmanager.Servers = testCase.upstreams
_ = setupUpstreams()
err := setupUpstreams()
if err != nil {
t.Error(err)
}
log.SetLevel(log.FatalLevel)
pullFromAlertmanager()
r := ginTestEngine()
@@ -1060,7 +1609,7 @@ func TestUpstreamStatus(t *testing.T) {
}
ur := models.AlertsResponse{}
err := json.Unmarshal(resp.Body.Bytes(), &ur)
err = json.Unmarshal(resp.Body.Bytes(), &ur)
if err != nil {
t.Errorf("Failed to unmarshal response: %s", err)
}
+2 -2
View File
@@ -22,7 +22,7 @@ func init() {
for _, version := range mock.ListAllMocks() {
name := fmt.Sprintf("dedup-mock-%s", version)
uri := fmt.Sprintf("http://%s.localhost", version)
am, err := alertmanager.NewAlertmanager(name, uri, alertmanager.WithRequestTimeout(time.Second))
am, err := alertmanager.NewAlertmanager("cluster", name, uri, alertmanager.WithRequestTimeout(time.Second))
if err != nil {
log.Fatal(err)
}
@@ -185,7 +185,7 @@ func TestClearData(t *testing.T) {
for _, version := range mock.ListAllMocks() {
name := fmt.Sprintf("clear-data-mock-%s", version)
uri := fmt.Sprintf("http://localhost/clear/%s", version)
am, _ := alertmanager.NewAlertmanager(name, uri, alertmanager.WithRequestTimeout(time.Second))
am, _ := alertmanager.NewAlertmanager("cluster", name, uri, alertmanager.WithRequestTimeout(time.Second))
mock.RegisterURL(fmt.Sprintf("%s/metrics", uri), version, "metrics")
_ = am.Pull()
+1 -1
View File
@@ -87,7 +87,7 @@ var uriTests = []uriTest{
func TestAlertmanagerURI(t *testing.T) {
for i, test := range uriTests {
am, err := NewAlertmanager("test", test.rawURI, WithExternalURI(test.extURI), WithProxy(test.proxy))
am, err := NewAlertmanager("cluster", "test", test.rawURI, WithExternalURI(test.extURI), WithProxy(test.proxy))
if err != nil {
t.Error(err)
}
+1
View File
@@ -36,6 +36,7 @@ type Alertmanager struct {
URI string `json:"uri"`
ExternalURI string `json:"-"`
RequestTimeout time.Duration `json:"timeout"`
Cluster string `json:"cluster"`
Name string `json:"name"`
// whenever this instance should be proxied
ProxyRequests bool `json:"proxyRequests"`
+2 -1
View File
@@ -22,11 +22,12 @@ var (
)
// NewAlertmanager creates a new Alertmanager instance
func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, error) {
func NewAlertmanager(cluster, name, upstreamURI string, opts ...Option) (*Alertmanager, error) {
am := &Alertmanager{
URI: upstreamURI,
ExternalURI: "",
RequestTimeout: time.Second * 10,
Cluster: cluster,
Name: name,
lock: sync.RWMutex{},
alertGroups: []models.AlertGroup{},
+4
View File
@@ -15,6 +15,7 @@ type testCase struct {
var testCases = []testCase{
{
config: config.AlertmanagerConfig{
Cluster: "cluster",
Name: "name",
URI: "http://localhost:9093",
ExternalURI: "http://localhost:9093",
@@ -26,6 +27,7 @@ var testCases = []testCase{
},
{
config: config.AlertmanagerConfig{
Cluster: "cluster",
Name: "proxy",
URI: "http://localhost:9094",
ExternalURI: "http://localhost:9094",
@@ -37,6 +39,7 @@ var testCases = []testCase{
},
{
config: config.AlertmanagerConfig{
Cluster: "cluster",
Name: "name",
URI: "http://localhost:9095",
ExternalURI: "http://localhost:9095",
@@ -60,6 +63,7 @@ func TestOptions(t *testing.T) {
}
am, err := NewAlertmanager(
tc.config.Cluster,
tc.config.Name,
tc.config.URI,
WithExternalURI(tc.config.ExternalURI),
+35
View File
@@ -34,3 +34,38 @@ func StringSliceToSHA1(stringArray []string) (string, error) {
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func StringSliceDiff(slice1 []string, slice2 []string) ([]string, []string) {
missing := []string{}
extra := []string{}
var found bool
for _, s1 := range slice1 {
found = false
for _, s2 := range slice2 {
if s1 == s2 {
found = true
break
}
}
if !found {
missing = append(missing, s1)
}
}
for _, s2 := range slice2 {
found = false
for _, s1 := range slice1 {
if s2 == s1 {
found = true
break
}
}
if !found {
extra = append(extra, s2)
}
}
return missing, extra
}
+48
View File
@@ -4,6 +4,8 @@ import (
"testing"
"github.com/prymitive/karma/internal/slices"
"github.com/google/go-cmp/cmp"
)
type stringSliceTest struct {
@@ -121,3 +123,49 @@ func TestStringSliceToSHA1(t *testing.T) {
t.Errorf("StringSliceToSHA1() returned empty string")
}
}
func TestStringSliceDiff(t *testing.T) {
type testCaseT struct {
a []string
b []string
missing []string
extra []string
}
testCases := []testCaseT{
{
a: []string{"a"},
b: []string{"a"},
missing: []string{},
extra: []string{},
},
{
a: []string{},
b: []string{"a"},
missing: []string{},
extra: []string{"a"},
},
{
a: []string{"a", "b"},
b: []string{"a"},
missing: []string{"b"},
extra: []string{},
},
{
a: []string{"a", "b"},
b: []string{"c"},
missing: []string{"a", "b"},
extra: []string{"c"},
},
}
for _, testCase := range testCases {
missing, extra := slices.StringSliceDiff(testCase.a, testCase.b)
if diff := cmp.Diff(testCase.missing, missing); diff != "" {
t.Errorf("Incorrect slice diff missing (-want +got):\n%s", diff)
}
if diff := cmp.Diff(testCase.extra, extra); diff != "" {
t.Errorf("Incorrect slice diff extra (-want +got):\n%s", diff)
}
}
}