Skip to content

Commit a532656

Browse files
committed
ARO-17482 resolve PR comments
1 parent dae326c commit a532656

File tree

11 files changed

+680
-45
lines changed

11 files changed

+680
-45
lines changed

admin/server/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ require (
5858
github.com/google/uuid v1.6.0 // indirect
5959
github.com/gorilla/css v1.0.1 // indirect
6060
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
61+
github.com/hashicorp/go-version v1.7.0 // indirect
6162
github.com/inconshreveable/mousetrap v1.1.0 // indirect
6263
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
6364
github.com/josharian/intern v1.0.0 // indirect

admin/server/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
106106
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
107107
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
108108
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
109+
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
110+
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
109111
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
110112
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
111113
github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ=

admin/server/handlers/hcp/breakglass/create.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package breakglass
1616

1717
import (
1818
"encoding/json"
19-
"errors"
2019
"fmt"
2120
"net/http"
2221
"time"
@@ -25,8 +24,7 @@ import (
2524
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2625
"k8s.io/utils/set"
2726

28-
ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"
29-
27+
hcphelpers "github.com/Azure/ARO-HCP/admin/server/handlers/hcp"
3028
"github.com/Azure/ARO-HCP/admin/server/middleware"
3129
"github.com/Azure/ARO-HCP/internal/api/arm"
3230
"github.com/Azure/ARO-HCP/internal/database"
@@ -73,12 +71,12 @@ func (h *HCPBreakglassSessionCreationHandler) ServeHTTP(writer http.ResponseWrit
7371

7472
clusterHypershiftDetails, err := h.csClient.GetClusterHypershiftDetails(request.Context(), hcp.ServiceProviderProperties.ClusterServiceID)
7573
if err != nil {
76-
return clusterServiceError(err, "hypershift details")
74+
return hcphelpers.ClusterServiceError(err, "hypershift details")
7775
}
7876

7977
provisionShard, err := h.csClient.GetClusterProvisionShard(request.Context(), hcp.ServiceProviderProperties.ClusterServiceID)
8078
if err != nil {
81-
return clusterServiceError(err, "provision shard")
79+
return hcphelpers.ClusterServiceError(err, "provision shard")
8280
}
8381

8482
group, ttl, err := h.validateSessionParameters(request)
@@ -185,16 +183,3 @@ func (h *HCPBreakglassSessionCreationHandler) validateSessionParameters(request
185183

186184
return body.Group, ttl, utilerrors.NewAggregate(errs)
187185
}
188-
189-
// clusterServiceError checks if err is an OCM not-found error and returns a
190-
// specific CloudError. This prevents ReportError from misinterpreting it as
191-
// "HCP resource not found" (the HCP was already found in the database).
192-
// Non-OCM errors are wrapped for ReportError to handle as internal errors.
193-
func clusterServiceError(err error, what string) error {
194-
var ocmErr *ocmerrors.Error
195-
if errors.As(err, &ocmErr) && ocmErr.Status() == http.StatusNotFound {
196-
return arm.NewCloudError(http.StatusNotFound, arm.CloudErrorCodeNotFound, "",
197-
"%s not found in cluster service", what)
198-
}
199-
return fmt.Errorf("failed to get %s from cluster service: %w", what, err)
200-
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hcp
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"net/http"
21+
22+
ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"
23+
24+
"github.com/Azure/ARO-HCP/internal/api/arm"
25+
)
26+
27+
// ClusterServiceError checks if err is an OCM not-found error and returns a
28+
// specific CloudError. This prevents ReportError from misinterpreting it as
29+
// "HCP resource not found" (the HCP was already found in the database).
30+
// Non-OCM errors are wrapped for ReportError to handle as internal errors.
31+
func ClusterServiceError(err error, what string) error {
32+
var ocmErr *ocmerrors.Error
33+
if errors.As(err, &ocmErr) && ocmErr.Status() == http.StatusNotFound {
34+
return arm.NewCloudError(http.StatusNotFound, arm.CloudErrorCodeNotFound, "",
35+
"%s not found in cluster service", what)
36+
}
37+
return fmt.Errorf("failed to get %s from cluster service: %w", what, err)
38+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hcp
16+
17+
import (
18+
"errors"
19+
"net/http"
20+
"testing"
21+
22+
ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"
23+
24+
"github.com/Azure/ARO-HCP/internal/api/arm"
25+
)
26+
27+
func TestClusterServiceError(t *testing.T) {
28+
// Helper to create OCM errors
29+
createOCMError := func(status int) error {
30+
ocmErr, err := ocmerrors.NewError().Status(status).Build()
31+
if err != nil {
32+
t.Fatalf("Failed to create OCM error: %v", err)
33+
}
34+
return ocmErr
35+
}
36+
37+
tests := []struct {
38+
name string
39+
err error
40+
what string
41+
expectedStatusCode int
42+
expectedErrorCode string
43+
expectedMessage string
44+
isCloudError bool
45+
}{
46+
{
47+
name: "OCM not-found error returns 404 CloudError",
48+
err: createOCMError(http.StatusNotFound),
49+
what: "cluster data",
50+
expectedStatusCode: http.StatusNotFound,
51+
expectedErrorCode: arm.CloudErrorCodeNotFound,
52+
expectedMessage: "cluster data not found in cluster service",
53+
isCloudError: true,
54+
},
55+
{
56+
name: "OCM not-found error for hypershift details",
57+
err: createOCMError(http.StatusNotFound),
58+
what: "hypershift details",
59+
expectedStatusCode: http.StatusNotFound,
60+
expectedErrorCode: arm.CloudErrorCodeNotFound,
61+
expectedMessage: "hypershift details not found in cluster service",
62+
isCloudError: true,
63+
},
64+
{
65+
name: "OCM internal server error wraps error",
66+
err: createOCMError(http.StatusInternalServerError),
67+
what: "cluster data",
68+
expectedMessage: "failed to get cluster data from cluster service",
69+
isCloudError: false,
70+
},
71+
{
72+
name: "OCM bad request error wraps error",
73+
err: createOCMError(http.StatusBadRequest),
74+
what: "provision shard",
75+
expectedMessage: "failed to get provision shard from cluster service",
76+
isCloudError: false,
77+
},
78+
{
79+
name: "OCM unauthorized error wraps error",
80+
err: createOCMError(http.StatusUnauthorized),
81+
what: "cluster data",
82+
expectedMessage: "failed to get cluster data from cluster service",
83+
isCloudError: false,
84+
},
85+
{
86+
name: "non-OCM error wraps error",
87+
err: errors.New("network timeout"),
88+
what: "cluster data",
89+
expectedMessage: "failed to get cluster data from cluster service",
90+
isCloudError: false,
91+
},
92+
{
93+
name: "generic error wraps error",
94+
err: errors.New("connection refused"),
95+
what: "hypershift details",
96+
expectedMessage: "failed to get hypershift details from cluster service",
97+
isCloudError: false,
98+
},
99+
}
100+
101+
for _, tt := range tests {
102+
t.Run(tt.name, func(t *testing.T) {
103+
result := ClusterServiceError(tt.err, tt.what)
104+
105+
if result == nil {
106+
t.Fatal("Expected error but got nil")
107+
}
108+
109+
if tt.isCloudError {
110+
// Check if it's a CloudError
111+
var cloudErr *arm.CloudError
112+
if !errors.As(result, &cloudErr) {
113+
t.Fatalf("Expected CloudError but got %T: %v", result, result)
114+
}
115+
116+
if cloudErr.StatusCode != tt.expectedStatusCode {
117+
t.Errorf("Expected status code %d, got %d", tt.expectedStatusCode, cloudErr.StatusCode)
118+
}
119+
120+
if cloudErr.Code != tt.expectedErrorCode {
121+
t.Errorf("Expected error code %q, got %q", tt.expectedErrorCode, cloudErr.Code)
122+
}
123+
124+
if cloudErr.Message != tt.expectedMessage {
125+
t.Errorf("Expected message %q, got %q", tt.expectedMessage, cloudErr.Message)
126+
}
127+
} else {
128+
// Check if it's a wrapped error (not CloudError)
129+
var cloudErr *arm.CloudError
130+
if errors.As(result, &cloudErr) {
131+
t.Fatalf("Expected wrapped error but got CloudError: %v", result)
132+
}
133+
134+
// Verify error message contains expected text
135+
if !contains(result.Error(), tt.expectedMessage) {
136+
t.Errorf("Expected error to contain %q, got %q", tt.expectedMessage, result.Error())
137+
}
138+
139+
// Verify original error is wrapped
140+
if !errors.Is(result, tt.err) {
141+
t.Errorf("Expected error to wrap original error")
142+
}
143+
}
144+
})
145+
}
146+
}
147+
148+
// contains checks if a string contains a substring
149+
func contains(s, substr string) bool {
150+
for i := 0; i <= len(s)-len(substr); i++ {
151+
if s[i:i+len(substr)] == substr {
152+
return true
153+
}
154+
}
155+
return false
156+
}

admin/server/handlers/hcp/serialconsole.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/Azure/ARO-HCP/internal/fpa"
2727
"github.com/Azure/ARO-HCP/internal/ocm"
2828
"github.com/Azure/ARO-HCP/internal/utils"
29+
"github.com/Azure/ARO-HCP/internal/validation"
2930
)
3031

3132
// HCPSerialConsoleHandler handles requests to retrieve VM serial console logs
@@ -76,6 +77,15 @@ func (h *HCPSerialConsoleHandler) ServeHTTP(writer http.ResponseWriter, request
7677
)
7778
}
7879

80+
if !validation.IsValidAzureVMName(vmName) {
81+
return arm.NewCloudError(
82+
http.StatusBadRequest,
83+
arm.CloudErrorCodeInvalidRequestContent,
84+
"",
85+
"vmName contains invalid characters or format",
86+
)
87+
}
88+
7989
// get HCP details
8090
hcp, err := h.dbClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).Get(request.Context(), resourceID.Name)
8191
if err != nil {
@@ -85,19 +95,19 @@ func (h *HCPSerialConsoleHandler) ServeHTTP(writer http.ResponseWriter, request
8595
// get CS cluster data to retrieve tenant ID
8696
csCluster, err := h.csClient.GetCluster(request.Context(), hcp.ServiceProviderProperties.ClusterServiceID)
8797
if err != nil {
88-
return fmt.Errorf("failed to get CS cluster data: %w", err)
98+
return ClusterServiceError(err, "cluster data")
8999
}
90100

91101
// get FPA credentials for customer tenant
92102
tokenCredential, err := h.fpaCredentialRetriever.RetrieveCredential(csCluster.Azure().TenantID())
93103
if err != nil {
94-
return fmt.Errorf("failed to get FPA token credentials: %w", err)
104+
return fmt.Errorf("failed to retrieve Azure credentials: %w", err)
95105
}
96106

97107
// Create Azure Compute client for customer subscription
98108
computeClient, err := armcompute.NewVirtualMachinesClient(hcp.ID.SubscriptionID, tokenCredential, nil)
99109
if err != nil {
100-
return fmt.Errorf("failed to create compute client: %w", err)
110+
return fmt.Errorf("failed to create Azure compute client: %w", err)
101111
}
102112

103113
// Retrieve boot diagnostics data containing serial console blob URI
@@ -109,7 +119,25 @@ func (h *HCPSerialConsoleHandler) ServeHTTP(writer http.ResponseWriter, request
109119
nil,
110120
)
111121
if err != nil {
112-
return fmt.Errorf("failed to retrieve boot diagnostics data: %w", err)
122+
// Azure returns 404 when VM or resource group doesn't exist
123+
if database.IsResponseError(err, http.StatusNotFound) {
124+
return arm.NewCloudError(
125+
http.StatusNotFound,
126+
arm.CloudErrorCodeNotFound,
127+
"",
128+
"VM %s not found in resource group %s", vmName, managedResourceGroup,
129+
)
130+
}
131+
// Azure returns 409 when boot diagnostics is disabled
132+
if database.IsResponseError(err, http.StatusConflict) {
133+
return arm.NewCloudError(
134+
http.StatusConflict,
135+
arm.CloudErrorCodeConflict,
136+
"",
137+
"Diagnostics might be disabled for VM %s", vmName,
138+
)
139+
}
140+
return fmt.Errorf("failed to retrieve boot diagnostics data for VM %s: %w", vmName, err)
113141
}
114142

115143
// verify serial console log blob URI is available

0 commit comments

Comments
 (0)