Skip to content
This repository was archived by the owner on Dec 20, 2025. It is now read-only.

Commit c22cc72

Browse files
fix(web): add Retrofit2SyncCall.execute to BakeService (#1879) (#1880)
* refactor(web): use constructor injection in BakeService to pave the way for more testing * test(web): demonstrate behavior of BakeService and how it's not using RoscoService / retrofit2 properly * fix(web): add Retrofit2SyncCall.execute to BakeService since (as of #1866) RoscoService uses retrofit2. (cherry picked from commit d3dc67d) Co-authored-by: David Byron <82477955+dbyron-sf@users.noreply.github.com>
1 parent 70da372 commit c22cc72

File tree

2 files changed

+171
-4
lines changed

2 files changed

+171
-4
lines changed

gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/BakeService.groovy

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.netflix.spinnaker.gate.services
1818

1919
import com.fasterxml.jackson.annotation.JsonInclude
2020
import com.netflix.spinnaker.gate.services.internal.RoscoServiceSelector
21+
import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall
2122
import groovy.util.logging.Slf4j
2223
import org.springframework.beans.factory.annotation.Autowired
2324
import org.springframework.boot.context.properties.ConfigurationProperties
@@ -27,22 +28,26 @@ import org.springframework.stereotype.Component
2728
@Component
2829
@ConfigurationProperties('services.rosco.defaults')
2930
class BakeService {
30-
@Autowired(required = false)
3131
RoscoServiceSelector roscoServiceSelector
3232

3333
// Default bake options from configuration.
3434
List<BakeOptions> bakeOptions
3535
// If set, use bake options defined in gate.yml instead of calling rosco
3636
boolean useDefaultBakeOptions
3737

38+
@Autowired
39+
BakeService(Optional<RoscoServiceSelector> roscoServiceSelector) {
40+
this.roscoServiceSelector = roscoServiceSelector.orElse(null);
41+
}
42+
3843
def bakeOptions() {
3944
(roscoServiceSelector && !useDefaultBakeOptions) ?
40-
roscoServiceSelector.withLocation().bakeOptions() : bakeOptions
45+
Retrofit2SyncCall.execute(roscoServiceSelector.withLocation().bakeOptions()) : bakeOptions
4146
}
4247

4348
def bakeOptions(String cloudProvider) {
4449
if (roscoServiceSelector) {
45-
return roscoServiceSelector.withLocation().bakeOptions(cloudProvider)
50+
return Retrofit2SyncCall.execute(roscoServiceSelector.withLocation().bakeOptions(cloudProvider))
4651
}
4752
def bakeOpts = bakeOptions.find { it.cloudProvider == cloudProvider }
4853
if (bakeOpts) {
@@ -53,7 +58,7 @@ class BakeService {
5358

5459
String lookupLogs(String region, String statusId) {
5560
if (roscoServiceSelector) {
56-
def logsMap = roscoServiceSelector.withLocation(region).lookupLogs(region, statusId)
61+
def logsMap = Retrofit2SyncCall.execute(roscoServiceSelector.withLocation(region).lookupLogs(region, statusId))
5762

5863
if (logsMap?.logsContent) {
5964
return "<pre>$logsMap.logsContent</pre>"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2025 Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.gate.service;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
import com.fasterxml.jackson.core.JsonProcessingException;
25+
import com.fasterxml.jackson.core.type.TypeReference;
26+
import com.fasterxml.jackson.databind.ObjectMapper;
27+
import com.netflix.spinnaker.gate.services.BakeService;
28+
import com.netflix.spinnaker.gate.services.internal.RoscoService;
29+
import com.netflix.spinnaker.gate.services.internal.RoscoServiceSelector;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Optional;
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.TestInfo;
36+
import retrofit2.mock.Calls;
37+
38+
class BakeServiceTest {
39+
40+
private ObjectMapper objectMapper = new ObjectMapper();
41+
42+
private RoscoServiceSelector roscoServiceSelector = mock(RoscoServiceSelector.class);
43+
44+
private RoscoService roscoService = mock(RoscoService.class);
45+
46+
private BakeService bakeService = new BakeService(Optional.of(roscoServiceSelector));
47+
48+
private BakeService.BakeOptions bakeOption;
49+
50+
private List<BakeService.BaseImage> baseImages;
51+
52+
private List<BakeService.BakeOptions> bakeOptions;
53+
54+
@BeforeEach
55+
void init(TestInfo testInfo) throws JsonProcessingException {
56+
System.out.println("--------------- Test " + testInfo.getDisplayName());
57+
58+
when(roscoServiceSelector.withLocation(any())).thenReturn(roscoService);
59+
60+
bakeOption = new BakeService.BakeOptions();
61+
bakeOption.setCloudProvider("my-cloud-provider");
62+
63+
BakeService.BaseImage baseImage = new BakeService.BaseImage();
64+
baseImage.setId("image-id");
65+
baseImage.setShortDescription("short description");
66+
baseImage.setDetailedDescription("detailed description");
67+
baseImage.setDisplayName("display name");
68+
baseImage.setPackageType("package type");
69+
baseImage.setVmTypes(List.of("my-vm-type"));
70+
71+
baseImages = List.of(baseImage);
72+
bakeOption.setBaseImages(baseImages);
73+
74+
bakeOptions = List.of(bakeOption);
75+
}
76+
77+
@Test
78+
void testBakeOptions() {
79+
// given
80+
when(roscoService.bakeOptions()).thenReturn(Calls.response(bakeOptions));
81+
82+
// when
83+
List<BakeService.BakeOptions> result =
84+
(List<BakeService.BakeOptions>) bakeService.bakeOptions();
85+
86+
// then
87+
verify(roscoServiceSelector).withLocation(null);
88+
verify(roscoService).bakeOptions();
89+
}
90+
91+
@Test
92+
void testBakeOptionsWithCloudProvider() {
93+
// Something is fishy here.
94+
//
95+
// When roscoServiceSelector is null,
96+
// BakeService.bakeOptions(String cloudProvider) fairly clearly returns a
97+
// BakeOptions object (the first element of the bakeOptions member with a
98+
// matching cloud provider).
99+
//
100+
// When roscoServiceSelector isn't null, BakeService.bakeOptions(String cloudProvider) returns
101+
// whatever RoscoService.bakeOptions(cloudProvider) returns, which is a Map.
102+
//
103+
// Presumably the http response from GET /bakery/options/{cloudProvider} has
104+
// the same structure either way. If the return value of
105+
// BakeController.bakeOptions(String cloudProvider) (which is the return
106+
// value of BakeService.bakeOptions(String cloudProvider)) serializes to the
107+
// same structure, it does.
108+
//
109+
// Groovy is allowing this. By converting BakeService to java, we'd have to
110+
// choose a return type for BakeService.bakeOptions(String cloudProvider).
111+
// Pretty sure that would be BakeOptions, and then we'd have a choice about
112+
// whether to change the return type of RoscoService.bakeOptions(String
113+
// cloudProvider) to match (my preference), or leave it returning a Map and
114+
// convert in BakeService.bakeOptions. The corresponding rosco code is at
115+
// https://github.com/spinnaker/rosco/blob/2f62f092e0a14bd10f204987c497034f54a46182/rosco-web/src/main/groovy/com/netflix/spinnaker/rosco/controllers/BakeryController.groovy#L75,
116+
// and the return type from rosco is
117+
// https://github.com/spinnaker/rosco/blob/2f62f092e0a14bd10f204987c497034f54a46182/rosco-core/src/main/groovy/com/netflix/spinnaker/rosco/api/BakeOptions.groovy,
118+
// which is out of sync with the corresponding type in gate's BakeService,
119+
// but it's close enough that it's worth a shot.
120+
//
121+
// For now since this test is java, and we're explicitly testing with roscoServiceSelector not
122+
// null, let's convert from a Map to BakeOptions.
123+
124+
// given
125+
String cloudProvider = "cloud-provider";
126+
Map<String, Object> bakeOptionsMap =
127+
objectMapper.convertValue(bakeOption, new TypeReference<>() {});
128+
when(roscoService.bakeOptions(cloudProvider)).thenReturn(Calls.response(bakeOptionsMap));
129+
130+
// when
131+
Object resultObj = bakeService.bakeOptions(cloudProvider);
132+
133+
// then
134+
verify(roscoServiceSelector).withLocation(null);
135+
verify(roscoService).bakeOptions(cloudProvider);
136+
137+
BakeService.BakeOptions result =
138+
objectMapper.convertValue(resultObj, BakeService.BakeOptions.class);
139+
assertThat(result).usingRecursiveComparison().isEqualTo(bakeOption);
140+
}
141+
142+
@Test
143+
void testLookupLogs() {
144+
// given
145+
String region = "my-region";
146+
String statusId = "my-status-id";
147+
148+
String roscoLog = "rosco-log";
149+
Map<String, String> roscoResult = Map.of("logsContent", roscoLog);
150+
151+
when(roscoService.lookupLogs(region, statusId)).thenReturn(Calls.response(roscoResult));
152+
153+
// when
154+
String result = bakeService.lookupLogs(region, statusId);
155+
156+
// then
157+
verify(roscoServiceSelector).withLocation(region);
158+
verify(roscoService).lookupLogs(region, statusId);
159+
160+
assertThat(result).isEqualTo("<pre>" + roscoLog + "</pre>");
161+
}
162+
}

0 commit comments

Comments
 (0)