{"action":"created","comment":{"url":"https://api.github.com/repos/giantswarm/user-config/pulls/comments/39396408","id":39396408,"diff_hunk":"@@ -28,151 +29,355 @@ func ExampleDefinition() AppDefinition {\n \t}\n }\n \n-func testDiffCallWith(t *testing.T, newCfg, oldCfg AppDefinition, expectedInfos []DiffInfo) {\n-\tinfos := Diff(newCfg, oldCfg)\n+func testDiffCallWith(t *testing.T, oldDef, newDef V2AppDefinition, expectedDiffInfos []DiffInfo) {\n+\tdiffInfos := ServiceDiff(oldDef, newDef)\n \n-\tif len(infos) != len(expectedInfos) {\n-\t\tt.Fatalf(\"Expected %d item, got %d: %v\", len(expectedInfos), len(infos), infos)\n+\tif len(diffInfos) != len(expectedDiffInfos) {\n+\t\tt.Fatalf(\"Expected %d item, got %d: %#v\", len(expectedDiffInfos), len(diffInfos), diffInfos)\n \t}\n \n-\tif !reflect.DeepEqual(infos, expectedInfos) {\n-\t\tfor _, exp := range expectedInfos {\n-\t\t\tt.Logf(\"* expected diff: %v\\n\", exp)\n+\tif !reflect.DeepEqual(diffInfos, expectedDiffInfos) {\n+\t\tfor _, exp := range expectedDiffInfos {\n+\t\t\tt.Logf(\"* expected diff: %#v\\n\", exp)\n \t\t}\n \n-\t\tfor _, got := range infos {\n-\t\t\tt.Logf(\"* found diff: %v\\n\", got)\n+\t\tfor _, got := range diffInfos {\n+\t\t\tt.Logf(\"* found diff: %#v\\n\", got)\n \t\t}\n \t\tt.Fatalf(\"Found diffs do not match expected diffs!\")\n \t}\n }\n \n-func TestDiffAppRename(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.AppName = \"app#changed\"\n+func TestDiffNoDiff(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\tnewDef := V2ExampleDefinition()\n \n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoAppNameChanged, Name: []string{\"app\"}},\n-\t}\n+\texpectedDiffInfos := []DiffInfo{}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-func TestDiffServiceRename(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.Services[0].ServiceName = \"service1#changed\"\n-\n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoNodeAdded, Name: []string{\"app\", \"service1#changed\"}},\n-\t\tDiffInfo{Type: InfoNodeRemoved, Name: []string{\"app\", \"service1\"}},\n+func TestDiffServiceNameUpdated(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\toldDef.AppName = \"service\"\n+\tnewDef := V2ExampleDefinition()\n+\tnewDef.AppName = \"my-new-service-name\"\n+\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoServiceNameUpdated,\n+\t\t\tOld:  \"service\",\n+\t\t\tNew:  \"my-new-service-name\",\n+\t\t},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-func TestDiffComponentRename(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.Services[0].Components[0].ComponentName = \"service1component1#changed\"\n+func TestDiffComponentAdded(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\tnewDef := V2ExampleDefinition()\n+\tnewDef.Components[ComponentName(\"my-new-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"80/tcp\")},\n+\t}\n \n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoNodeAdded, Name: []string{\"app\", \"service1\", \"service1component1#changed\"}},\n-\t\tDiffInfo{Type: InfoNodeRemoved, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentAdded,\n+\t\t\tNew:  \"my-new-component\",\n+\t\t},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-func TestDiffComponentUpdateImage(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.Services[0].Components[0].Image = generictypes.MustParseDockerImage(\"landingpage2\")\n-\n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoInstanceConfigUpdated, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n+func TestDiffComponentRemoved(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\toldDef.Components[ComponentName(\"my-old-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"80/tcp\")},\n \t}\n+\tnewDef := V2ExampleDefinition()\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n-}\n-func TestDiffComponentUpdateArgs(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.Services[0].Components[0].Args = []string{\"--env=test\"}\n-\n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoInstanceConfigUpdated, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentRemoved,\n+\t\t\tOld:  \"my-old-component\",\n+\t\t},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-func TestDiffComponentScalingChanged(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n-\tnewCfg.Services[0].Components[0].ScalingPolicy = &ScaleDefinition{Min: 0, Max: 1000}\n+func TestDiffComponentAddedAndRemoved(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\toldDef.Components[ComponentName(\"my-old-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"80/tcp\")},\n+\t}\n+\tnewDef := V2ExampleDefinition()\n+\tnewDef.Components[ComponentName(\"my-new-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"80/tcp\")},\n+\t}\n \n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoComponentScalingUpdated, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentAdded,\n+\t\t\tNew:  \"my-new-component\",\n+\t\t},\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentRemoved,\n+\t\t\tOld:  \"my-old-component\",\n+\t\t},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-// Changes in the service should not be alert if the e.g. the service was renamed (yeah, illogical, I know)\n-func TestDiffLowerLevelChangesShouldBeIgnored(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n+func TestDiffComponentUpdated(t *testing.T) {\n+\toldDef := V2ExampleDefinition()\n+\toldDef.Components[ComponentName(\"my-old-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"80/tcp\")},\n+\t}\n+\tnewDef := V2ExampleDefinition()\n+\tnewDef.Components[ComponentName(\"my-old-component\")] = &ComponentDefinition{\n+\t\tImage: MustParseImageDefinition(\"registry.giantswarm.io/landingpage:0.10.0\"),\n+\t\tPorts: []generictypes.DockerPort{generictypes.MustParseDockerPort(\"8080/tcp\")}, // port updated\n+\t}\n \n-\tnewCfg.Services[0].Components = append(newCfg.Services[0].Components, ComponentConfig{\n-\t\tComponentName: \"service1component1\",\n-\t\tInstanceConfig: InstanceConfig{\n-\t\t\tImage: generictypes.MustParseDockerImage(\"foobar\"),\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentUpdated,\n+\t\t\tOld:  \"my-old-component\",\n+\t\t\tNew:  \"my-old-component\",\n \t\t},\n-\t})\n-\tnewCfg.Services[0].ServiceName = \"service1#changed\"\n-\n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoNodeAdded, Name: []string{\"app\", \"service1#changed\"}},\n-\t\tDiffInfo{Type: InfoNodeRemoved, Name: []string{\"app\", \"service1\"}},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n \n-func TestDiffMultipleChanges(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n+func TestDiffFullDefinitionUpdate(t *testing.T) {\n+\t// The original implementation (of \"diff\" creation) has an issue with the go\n+\t// implementation of map, not being consistent with respect to ordering of\n+\t// elements while iterating through the map. With this loop we prevent that\n+\t// it works \"by mistake\" the first time (but not the second or third time)\n+\tfor i := 0; i < 1000; i++ {\n+\t\trawOldDef := `\n+\t\t{\n+\t\t\t\"name\": \"redis-example\",\n+\t\t\t\"components\": {\n+\t\t\t\t\"redis\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t},\n+\t\t\t\t\"service\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis\", \"target_port\": 6379 }\n+\t\t\t\t\t]\n+\t\t\t\t},\n+\t\t\t\t\"redis2\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t},\n+\t\t\t\t\"service2\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis2\", \"target_port\": 6379 }\n+\t\t\t\t\t]\n+\t\t\t\t},\n+\t\t\t\t\"redis3\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t},\n+\t\t\t\t\"service3\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis3\", \"target_port\": 6379 }\n+\t\t\t\t\t]\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t`\n \n-\tnewCfg.Services[0].Components[0].ComponentName = \"service1component1#changed\"\n-\tnewCfg.Services[0].Components = append(newCfg.Services[0].Components, ComponentConfig{\n-\t\tComponentName: \"service1component2\",\n-\t\tInstanceConfig: InstanceConfig{\n-\t\t\tImage: generictypes.MustParseDockerImage(\"foobar\"),\n-\t\t},\n-\t})\n+\t\tvar oldDef V2AppDefinition\n+\t\tif err := json.Unmarshal([]byte(rawOldDef), &oldDef); err != nil {\n+\t\t\tt.Fatalf(\"failed to unmarshal service definition: %#v\", err)\n+\t\t}\n \n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoNodeAdded, Name: []string{\"app\", \"service1\", \"service1component1#changed\"}},\n-\t\tDiffInfo{Type: InfoNodeAdded, Name: []string{\"app\", \"service1\", \"service1component2\"}},\n-\t\tDiffInfo{Type: InfoNodeRemoved, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n-\t}\n+\t\trawNewDef := `\n+\t\t{\n+\t\t\t\"name\": \"redis-example-2\",\n+\t\t\t\"components\": {\n+\t\t\t\t\"redis1\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6000\n+\t\t\t\t},\n+\t\t\t\t\"service1\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"bar.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis1\", \"target_port\": 6000 }\n+\t\t\t\t\t]\n+\t\t\t\t},\n+\t\t\t\t\"redis2\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6000\n+\t\t\t\t},\n+\t\t\t\t\"service2\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis2\", \"target_port\": 6000 }\n+\t\t\t\t\t]\n+\t\t\t\t},\n+\t\t\t\t\"redis3\": {\n+\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t},\n+\t\t\t\t\"service3\": {\n+\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t{ \"component\": \"redis3\", \"target_port\": 6379 }\n+\t\t\t\t\t]\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t`\n+\n+\t\tvar newDef V2AppDefinition\n+\t\tif err := json.Unmarshal([]byte(rawNewDef), &newDef); err != nil {\n+\t\t\tt.Fatalf(\"failed to unmarshal service definition: %#v\", err)\n+\t\t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\t\texpectedDiffInfos := []DiffInfo{\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoServiceNameUpdated,\n+\t\t\t\tOld:  \"redis-example\",\n+\t\t\t\tNew:  \"redis-example-2\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentAdded,\n+\t\t\t\tNew:  \"redis1\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentAdded,\n+\t\t\t\tNew:  \"service1\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentUpdated,\n+\t\t\t\tOld:  \"redis2\",\n+\t\t\t\tNew:  \"redis2\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentUpdated,\n+\t\t\t\tOld:  \"service2\",\n+\t\t\t\tNew:  \"service2\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentRemoved,\n+\t\t\t\tOld:  \"redis\",\n+\t\t\t},\n+\t\t\tDiffInfo{\n+\t\t\t\tType: DiffInfoComponentRemoved,\n+\t\t\t\tOld:  \"service\",\n+\t\t\t},\n+\t\t}\n+\n+\t\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n+\t}\n }\n \n-func TestDiffComponentMultipleChanges(t *testing.T) {\n-\toldCfg := ExampleDefinition()\n-\tnewCfg := ExampleDefinition()\n+// Because of our smart data types some definitions can be represented\n+// differently. The following test ensures that a diff only is created in case\n+// the component really changed.\n+func TestDiffComponentDefinitionNoUpdate(t *testing.T) {\n+\trawOldDef := `\n+\t\t\t{\n+\t\t\t\t\"name\": \"redis-example\",\n+\t\t\t\t\"components\": {\n+\t\t\t\t\t\"redis\": {\n+\t\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\t\"ports\": [\n+\t\t\t\t\t\t\t6379\n+\t\t\t\t\t\t]\n+\t\t\t\t\t},\n+\t\t\t\t\t\"service\": {\n+\t\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\t\"ports\": [\n+\t\t\t\t\t\t\t80\n+\t\t\t\t\t\t],\n+\t\t\t\t\t\t\"domains\": {\n+\t\t\t\t\t\t\t\"80/tcp\": [\n+\t\t\t\t\t\t\t\t\"foo.com\"\n+\t\t\t\t\t\t\t]\n+\t\t\t\t\t\t},\n+\t\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t\t{\n+\t\t\t\t\t\t\t\t\"component\": \"redis\",\n+\t\t\t\t\t\t\t\t\"target_port\": 6379\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t]\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t`\n+\n+\tvar oldDef V2AppDefinition\n+\tif err := json.Unmarshal([]byte(rawOldDef), &oldDef); err != nil {\n+\t\tt.Fatalf(\"failed to unmarshal service definition: %#v\", err)\n+\t}\n \n-\tnewCfg.Services[0].Components[0].ScalingPolicy = &ScaleDefinition{Min: 10}\n-\tnewCfg.Services[0].Components[0].InstanceConfig.Image = generictypes.MustParseDockerImage(\"new-site\")\n+\trawNewDef := `\n+\t\t\t{\n+\t\t\t\t\"name\": \"redis-example\",\n+\t\t\t\t\"components\": {\n+\t\t\t\t\t\"redis\": {\n+\t\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t\t},\n+\t\t\t\t\t\"service\": {\n+\t\t\t\t\t\t\"image\": \"giantswarm/redis-example:0.3.0\",\n+\t\t\t\t\t\t\"ports\": 80,\n+\t\t\t\t\t\t\"domains\": { \"80\": \"foo.com\" },\n+\t\t\t\t\t\t\"links\": [\n+\t\t\t\t\t\t\t{ \"component\": \"redis\", \"target_port\": 6379 }\n+\t\t\t\t\t\t]\n+\t\t\t\t\t},\n+\t\t\t\t\t\"redis2\": {\n+\t\t\t\t\t\t\"image\": \"redis\",\n+\t\t\t\t\t\t\"ports\": 6379\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t`\n+\n+\tvar newDef V2AppDefinition\n+\tif err := json.Unmarshal([]byte(rawNewDef), &newDef); err != nil {\n+\t\tt.Fatalf(\"failed to unmarshal service definition: %#v\", err)\n+\t}\n \n-\texpectedDiffItems := []DiffInfo{\n-\t\tDiffInfo{Type: InfoInstanceConfigUpdated, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n-\t\tDiffInfo{Type: InfoComponentScalingUpdated, Name: []string{\"app\", \"service1\", \"service1component1\"}},\n+\texpectedDiffInfos := []DiffInfo{\n+\t\tDiffInfo{\n+\t\t\tType: DiffInfoComponentAdded,\n+\t\t\tNew:  \"redis2\",\n+\t\t},\n \t}\n \n-\ttestDiffCallWith(t, newCfg, oldCfg, expectedDiffItems)\n+\ttestDiffCallWith(t, oldDef, newDef, expectedDiffInfos)\n }\n+\n+// TODO test each definition difference inside a component definition","path":"diff_test.go","position":461,"original_position":461,"commit_id":"4bdf79a422427ebeb6a33b2360e5fbf173c31168","original_commit_id":"4bdf79a422427ebeb6a33b2360e5fbf173c31168","user":{"login":"zyndiecate","id":552769,"avatar_url":"https://avatars.githubusercontent.com/u/552769?v=3","gravatar_id":"","url":"https://api.github.com/users/zyndiecate","html_url":"https://github.com/zyndiecate","followers_url":"https://api.github.com/users/zyndiecate/followers","following_url":"https://api.github.com/users/zyndiecate/following{/other_user}","gists_url":"https://api.github.com/users/zyndiecate/gists{/gist_id}","starred_url":"https://api.github.com/users/zyndiecate/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zyndiecate/subscriptions","organizations_url":"https://api.github.com/users/zyndiecate/orgs","repos_url":"https://api.github.com/users/zyndiecate/repos","events_url":"https://api.github.com/users/zyndiecate/events{/privacy}","received_events_url":"https://api.github.com/users/zyndiecate/received_events","type":"User","site_admin":false},"body":"See https://github.com/giantswarm/user-config/issues/61","created_at":"2015-09-14T13:59:11Z","updated_at":"2015-09-14T13:59:11Z","html_url":"https://github.com/giantswarm/user-config/pull/60#discussion_r39396408","pull_request_url":"https://api.github.com/repos/giantswarm/user-config/pulls/60","_links":{"self":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/comments/39396408"},"html":{"href":"https://github.com/giantswarm/user-config/pull/60#discussion_r39396408"},"pull_request":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/60"}}},"pull_request":{"url":"https://api.github.com/repos/giantswarm/user-config/pulls/60","id":44560660,"html_url":"https://github.com/giantswarm/user-config/pull/60","diff_url":"https://github.com/giantswarm/user-config/pull/60.diff","patch_url":"https://github.com/giantswarm/user-config/pull/60.patch","issue_url":"https://api.github.com/repos/giantswarm/user-config/issues/60","number":60,"state":"open","locked":false,"title":"basic diff to detect differences between service definitions","user":{"login":"zyndiecate","id":552769,"avatar_url":"https://avatars.githubusercontent.com/u/552769?v=3","gravatar_id":"","url":"https://api.github.com/users/zyndiecate","html_url":"https://github.com/zyndiecate","followers_url":"https://api.github.com/users/zyndiecate/followers","following_url":"https://api.github.com/users/zyndiecate/following{/other_user}","gists_url":"https://api.github.com/users/zyndiecate/gists{/gist_id}","starred_url":"https://api.github.com/users/zyndiecate/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zyndiecate/subscriptions","organizations_url":"https://api.github.com/users/zyndiecate/orgs","repos_url":"https://api.github.com/users/zyndiecate/repos","events_url":"https://api.github.com/users/zyndiecate/events{/privacy}","received_events_url":"https://api.github.com/users/zyndiecate/received_events","type":"User","site_admin":false},"body":"This implements basic diff functionality to detect differences between service definitions. This will be necessary to implement service definition updates. ","created_at":"2015-09-09T15:00:09Z","updated_at":"2015-09-14T13:59:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"6096620febe039cde81281a86c98e8606bdcf32c","assignee":{"login":"zyndiecate","id":552769,"avatar_url":"https://avatars.githubusercontent.com/u/552769?v=3","gravatar_id":"","url":"https://api.github.com/users/zyndiecate","html_url":"https://github.com/zyndiecate","followers_url":"https://api.github.com/users/zyndiecate/followers","following_url":"https://api.github.com/users/zyndiecate/following{/other_user}","gists_url":"https://api.github.com/users/zyndiecate/gists{/gist_id}","starred_url":"https://api.github.com/users/zyndiecate/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zyndiecate/subscriptions","organizations_url":"https://api.github.com/users/zyndiecate/orgs","repos_url":"https://api.github.com/users/zyndiecate/repos","events_url":"https://api.github.com/users/zyndiecate/events{/privacy}","received_events_url":"https://api.github.com/users/zyndiecate/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/giantswarm/user-config/pulls/60/commits","review_comments_url":"https://api.github.com/repos/giantswarm/user-config/pulls/60/comments","review_comment_url":"https://api.github.com/repos/giantswarm/user-config/pulls/comments{/number}","comments_url":"https://api.github.com/repos/giantswarm/user-config/issues/60/comments","statuses_url":"https://api.github.com/repos/giantswarm/user-config/statuses/4bdf79a422427ebeb6a33b2360e5fbf173c31168","head":{"label":"giantswarm:diff","ref":"diff","sha":"4bdf79a422427ebeb6a33b2360e5fbf173c31168","user":{"login":"giantswarm","id":7556340,"avatar_url":"https://avatars.githubusercontent.com/u/7556340?v=3","gravatar_id":"","url":"https://api.github.com/users/giantswarm","html_url":"https://github.com/giantswarm","followers_url":"https://api.github.com/users/giantswarm/followers","following_url":"https://api.github.com/users/giantswarm/following{/other_user}","gists_url":"https://api.github.com/users/giantswarm/gists{/gist_id}","starred_url":"https://api.github.com/users/giantswarm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/giantswarm/subscriptions","organizations_url":"https://api.github.com/users/giantswarm/orgs","repos_url":"https://api.github.com/users/giantswarm/repos","events_url":"https://api.github.com/users/giantswarm/events{/privacy}","received_events_url":"https://api.github.com/users/giantswarm/received_events","type":"Organization","site_admin":false},"repo":{"id":30184290,"name":"user-config","full_name":"giantswarm/user-config","owner":{"login":"giantswarm","id":7556340,"avatar_url":"https://avatars.githubusercontent.com/u/7556340?v=3","gravatar_id":"","url":"https://api.github.com/users/giantswarm","html_url":"https://github.com/giantswarm","followers_url":"https://api.github.com/users/giantswarm/followers","following_url":"https://api.github.com/users/giantswarm/following{/other_user}","gists_url":"https://api.github.com/users/giantswarm/gists{/gist_id}","starred_url":"https://api.github.com/users/giantswarm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/giantswarm/subscriptions","organizations_url":"https://api.github.com/users/giantswarm/orgs","repos_url":"https://api.github.com/users/giantswarm/repos","events_url":"https://api.github.com/users/giantswarm/events{/privacy}","received_events_url":"https://api.github.com/users/giantswarm/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/giantswarm/user-config","description":"Data structures used for the Giant Swarm service definition (swarm.json)","fork":false,"url":"https://api.github.com/repos/giantswarm/user-config","forks_url":"https://api.github.com/repos/giantswarm/user-config/forks","keys_url":"https://api.github.com/repos/giantswarm/user-config/keys{/key_id}","collaborators_url":"https://api.github.com/repos/giantswarm/user-config/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/giantswarm/user-config/teams","hooks_url":"https://api.github.com/repos/giantswarm/user-config/hooks","issue_events_url":"https://api.github.com/repos/giantswarm/user-config/issues/events{/number}","events_url":"https://api.github.com/repos/giantswarm/user-config/events","assignees_url":"https://api.github.com/repos/giantswarm/user-config/assignees{/user}","branches_url":"https://api.github.com/repos/giantswarm/user-config/branches{/branch}","tags_url":"https://api.github.com/repos/giantswarm/user-config/tags","blobs_url":"https://api.github.com/repos/giantswarm/user-config/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/giantswarm/user-config/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/giantswarm/user-config/git/refs{/sha}","trees_url":"https://api.github.com/repos/giantswarm/user-config/git/trees{/sha}","statuses_url":"https://api.github.com/repos/giantswarm/user-config/statuses/{sha}","languages_url":"https://api.github.com/repos/giantswarm/user-config/languages","stargazers_url":"https://api.github.com/repos/giantswarm/user-config/stargazers","contributors_url":"https://api.github.com/repos/giantswarm/user-config/contributors","subscribers_url":"https://api.github.com/repos/giantswarm/user-config/subscribers","subscription_url":"https://api.github.com/repos/giantswarm/user-config/subscription","commits_url":"https://api.github.com/repos/giantswarm/user-config/commits{/sha}","git_commits_url":"https://api.github.com/repos/giantswarm/user-config/git/commits{/sha}","comments_url":"https://api.github.com/repos/giantswarm/user-config/comments{/number}","issue_comment_url":"https://api.github.com/repos/giantswarm/user-config/issues/comments{/number}","contents_url":"https://api.github.com/repos/giantswarm/user-config/contents/{+path}","compare_url":"https://api.github.com/repos/giantswarm/user-config/compare/{base}...{head}","merges_url":"https://api.github.com/repos/giantswarm/user-config/merges","archive_url":"https://api.github.com/repos/giantswarm/user-config/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/giantswarm/user-config/downloads","issues_url":"https://api.github.com/repos/giantswarm/user-config/issues{/number}","pulls_url":"https://api.github.com/repos/giantswarm/user-config/pulls{/number}","milestones_url":"https://api.github.com/repos/giantswarm/user-config/milestones{/number}","notifications_url":"https://api.github.com/repos/giantswarm/user-config/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/giantswarm/user-config/labels{/name}","releases_url":"https://api.github.com/repos/giantswarm/user-config/releases{/id}","created_at":"2015-02-02T11:36:25Z","updated_at":"2015-09-02T07:14:52Z","pushed_at":"2015-09-14T13:31:55Z","git_url":"git://github.com/giantswarm/user-config.git","ssh_url":"git@github.com:giantswarm/user-config.git","clone_url":"https://github.com/giantswarm/user-config.git","svn_url":"https://github.com/giantswarm/user-config","homepage":"","size":1055,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":12,"forks":0,"open_issues":12,"watchers":0,"default_branch":"master"}},"base":{"label":"giantswarm:master","ref":"master","sha":"0558e68ca6cbd70f17e5ae480e6717f4f3448c29","user":{"login":"giantswarm","id":7556340,"avatar_url":"https://avatars.githubusercontent.com/u/7556340?v=3","gravatar_id":"","url":"https://api.github.com/users/giantswarm","html_url":"https://github.com/giantswarm","followers_url":"https://api.github.com/users/giantswarm/followers","following_url":"https://api.github.com/users/giantswarm/following{/other_user}","gists_url":"https://api.github.com/users/giantswarm/gists{/gist_id}","starred_url":"https://api.github.com/users/giantswarm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/giantswarm/subscriptions","organizations_url":"https://api.github.com/users/giantswarm/orgs","repos_url":"https://api.github.com/users/giantswarm/repos","events_url":"https://api.github.com/users/giantswarm/events{/privacy}","received_events_url":"https://api.github.com/users/giantswarm/received_events","type":"Organization","site_admin":false},"repo":{"id":30184290,"name":"user-config","full_name":"giantswarm/user-config","owner":{"login":"giantswarm","id":7556340,"avatar_url":"https://avatars.githubusercontent.com/u/7556340?v=3","gravatar_id":"","url":"https://api.github.com/users/giantswarm","html_url":"https://github.com/giantswarm","followers_url":"https://api.github.com/users/giantswarm/followers","following_url":"https://api.github.com/users/giantswarm/following{/other_user}","gists_url":"https://api.github.com/users/giantswarm/gists{/gist_id}","starred_url":"https://api.github.com/users/giantswarm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/giantswarm/subscriptions","organizations_url":"https://api.github.com/users/giantswarm/orgs","repos_url":"https://api.github.com/users/giantswarm/repos","events_url":"https://api.github.com/users/giantswarm/events{/privacy}","received_events_url":"https://api.github.com/users/giantswarm/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/giantswarm/user-config","description":"Data structures used for the Giant Swarm service definition (swarm.json)","fork":false,"url":"https://api.github.com/repos/giantswarm/user-config","forks_url":"https://api.github.com/repos/giantswarm/user-config/forks","keys_url":"https://api.github.com/repos/giantswarm/user-config/keys{/key_id}","collaborators_url":"https://api.github.com/repos/giantswarm/user-config/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/giantswarm/user-config/teams","hooks_url":"https://api.github.com/repos/giantswarm/user-config/hooks","issue_events_url":"https://api.github.com/repos/giantswarm/user-config/issues/events{/number}","events_url":"https://api.github.com/repos/giantswarm/user-config/events","assignees_url":"https://api.github.com/repos/giantswarm/user-config/assignees{/user}","branches_url":"https://api.github.com/repos/giantswarm/user-config/branches{/branch}","tags_url":"https://api.github.com/repos/giantswarm/user-config/tags","blobs_url":"https://api.github.com/repos/giantswarm/user-config/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/giantswarm/user-config/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/giantswarm/user-config/git/refs{/sha}","trees_url":"https://api.github.com/repos/giantswarm/user-config/git/trees{/sha}","statuses_url":"https://api.github.com/repos/giantswarm/user-config/statuses/{sha}","languages_url":"https://api.github.com/repos/giantswarm/user-config/languages","stargazers_url":"https://api.github.com/repos/giantswarm/user-config/stargazers","contributors_url":"https://api.github.com/repos/giantswarm/user-config/contributors","subscribers_url":"https://api.github.com/repos/giantswarm/user-config/subscribers","subscription_url":"https://api.github.com/repos/giantswarm/user-config/subscription","commits_url":"https://api.github.com/repos/giantswarm/user-config/commits{/sha}","git_commits_url":"https://api.github.com/repos/giantswarm/user-config/git/commits{/sha}","comments_url":"https://api.github.com/repos/giantswarm/user-config/comments{/number}","issue_comment_url":"https://api.github.com/repos/giantswarm/user-config/issues/comments{/number}","contents_url":"https://api.github.com/repos/giantswarm/user-config/contents/{+path}","compare_url":"https://api.github.com/repos/giantswarm/user-config/compare/{base}...{head}","merges_url":"https://api.github.com/repos/giantswarm/user-config/merges","archive_url":"https://api.github.com/repos/giantswarm/user-config/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/giantswarm/user-config/downloads","issues_url":"https://api.github.com/repos/giantswarm/user-config/issues{/number}","pulls_url":"https://api.github.com/repos/giantswarm/user-config/pulls{/number}","milestones_url":"https://api.github.com/repos/giantswarm/user-config/milestones{/number}","notifications_url":"https://api.github.com/repos/giantswarm/user-config/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/giantswarm/user-config/labels{/name}","releases_url":"https://api.github.com/repos/giantswarm/user-config/releases{/id}","created_at":"2015-02-02T11:36:25Z","updated_at":"2015-09-02T07:14:52Z","pushed_at":"2015-09-14T13:31:55Z","git_url":"git://github.com/giantswarm/user-config.git","ssh_url":"git@github.com:giantswarm/user-config.git","clone_url":"https://github.com/giantswarm/user-config.git","svn_url":"https://github.com/giantswarm/user-config","homepage":"","size":1055,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":12,"forks":0,"open_issues":12,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/60"},"html":{"href":"https://github.com/giantswarm/user-config/pull/60"},"issue":{"href":"https://api.github.com/repos/giantswarm/user-config/issues/60"},"comments":{"href":"https://api.github.com/repos/giantswarm/user-config/issues/60/comments"},"review_comments":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/60/comments"},"review_comment":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/giantswarm/user-config/pulls/60/commits"},"statuses":{"href":"https://api.github.com/repos/giantswarm/user-config/statuses/4bdf79a422427ebeb6a33b2360e5fbf173c31168"}}}}