Skip to content

Commit 95c1da4

Browse files
authored
Merge branch 'master' into usr/akhil/GITOPS-9256
2 parents c19caa6 + 71974fd commit 95c1da4

11 files changed

+623
-64
lines changed

controllers/argocd/openshift/openshift.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ func ReconcilerHook(cr *argoapp.ArgoCD, v interface{}, hint string) error {
6363
case cr.Name + "-repo-server":
6464

6565
prodImage := o.Spec.Template.Spec.Containers[0].Image
66-
usingReleasedImages := strings.Contains(prodImage, "registry.redhat.io/openshift-gitops-1/argocd-rhel")
67-
if cr.Spec.Repo.SystemCATrust != nil && usingReleasedImages {
66+
if cr.Spec.Repo.SystemCATrust != nil {
6867
updateSystemCATrustBuilding(cr, o, prodImage, logv)
6968
}
7069
}
@@ -154,7 +153,8 @@ done
154153
echo "User defined trusted CA files:"
155154
ls /etc/pki/ca-trust/source/anchors/
156155
157-
update-ca-trust
156+
# Specifying the explicit location to turn on the container-aware behavior
157+
update-ca-trust extract --output /etc/pki/ca-trust/extracted
158158
159159
echo "Trusted anchors:"
160160
trust list

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/go-logr/logr v1.4.3
1212
github.com/google/go-cmp v0.7.0
1313
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518
14+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
1415
github.com/hashicorp/go-version v1.7.0
1516
github.com/onsi/ginkgo/v2 v2.28.1
1617
github.com/onsi/gomega v1.39.1
@@ -101,7 +102,6 @@ require (
101102
github.com/google/go-github/v75 v75.0.0 // indirect
102103
github.com/google/go-querystring v1.1.0 // indirect
103104
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
104-
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
105105
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
106106
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
107107
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
Copyright 2026.
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+
17+
package argocdclient
18+
19+
import (
20+
"crypto/tls"
21+
"encoding/json"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"net/http"
26+
"net/url"
27+
"strings"
28+
"sync"
29+
"time"
30+
31+
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
32+
"github.com/gorilla/websocket"
33+
)
34+
35+
// TerminalClient represents a test client for terminal WebSocket connections.
36+
type TerminalClient struct {
37+
wsConn *websocket.Conn
38+
mu sync.Mutex
39+
closed bool
40+
output strings.Builder
41+
outputMu sync.Mutex
42+
}
43+
44+
// ExecTerminal opens a terminal session to a pod via WebSocket.
45+
// This replicates the behavior of the ArgoCD UI when a user opens a terminal session to an application.
46+
// ArgoCD decides which shell to use based on the configured allowed shells.
47+
func ExecTerminal(endpoint, token string, app *v1alpha1.Application, namespace, podName, container string) (*TerminalClient, error) {
48+
u := &url.URL{
49+
Scheme: "wss",
50+
Host: endpoint,
51+
Path: "/terminal",
52+
}
53+
54+
q := u.Query()
55+
q.Set("pod", podName)
56+
q.Set("container", container)
57+
q.Set("appName", app.Name)
58+
q.Set("appNamespace", app.Namespace)
59+
q.Set("projectName", app.Spec.Project)
60+
q.Set("namespace", namespace)
61+
u.RawQuery = q.Encode()
62+
63+
dialer := websocket.Dialer{
64+
TLSClientConfig: &tls.Config{
65+
InsecureSkipVerify: true, // #nosec G402
66+
},
67+
}
68+
69+
headers := http.Header{}
70+
headers.Set("Cookie", fmt.Sprintf("argocd.token=%s", token))
71+
72+
wsConn, resp, err := dialer.Dial(u.String(), headers)
73+
if err != nil {
74+
if resp != nil {
75+
defer resp.Body.Close()
76+
body, _ := io.ReadAll(resp.Body)
77+
return nil, fmt.Errorf("failed to connect to terminal WebSocket: %w (status: %d, body: %s)", err, resp.StatusCode, string(body))
78+
}
79+
return nil, fmt.Errorf("failed to connect to terminal WebSocket: %w", err)
80+
}
81+
82+
session := &TerminalClient{
83+
wsConn: wsConn,
84+
}
85+
86+
go session.readOutput()
87+
88+
return session, nil
89+
}
90+
91+
// terminalMessage is the JSON message format used by ArgoCD terminal WebSocket
92+
type terminalMessage struct {
93+
Operation string `json:"operation"`
94+
Data string `json:"data"`
95+
Rows uint16 `json:"rows"`
96+
Cols uint16 `json:"cols"`
97+
}
98+
99+
// readOutput continuously reads output from the WebSocket connection
100+
func (s *TerminalClient) readOutput() {
101+
for {
102+
_, message, err := s.wsConn.ReadMessage()
103+
if err != nil {
104+
// Connection closed or error
105+
return
106+
}
107+
108+
if len(message) < 1 {
109+
continue
110+
}
111+
112+
// Parse JSON message
113+
var msg terminalMessage
114+
if err := json.Unmarshal(message, &msg); err != nil {
115+
continue
116+
}
117+
118+
switch msg.Operation {
119+
case "stdout":
120+
s.outputMu.Lock()
121+
s.output.WriteString(msg.Data)
122+
s.outputMu.Unlock()
123+
}
124+
}
125+
}
126+
127+
// SendInput sends input to the terminal session
128+
func (s *TerminalClient) SendInput(input string) error {
129+
s.mu.Lock()
130+
defer s.mu.Unlock()
131+
132+
if s.closed {
133+
return errors.New("session is closed")
134+
}
135+
136+
// ArgoCD terminal uses JSON messages (includes rows/cols like the UI)
137+
msg, err := json.Marshal(terminalMessage{
138+
Operation: "stdin",
139+
Data: input,
140+
Rows: 24,
141+
Cols: 80,
142+
})
143+
if err != nil {
144+
return err
145+
}
146+
return s.wsConn.WriteMessage(websocket.TextMessage, msg)
147+
}
148+
149+
// SendResize sends a terminal resize message
150+
func (s *TerminalClient) SendResize(cols, rows uint16) error {
151+
s.mu.Lock()
152+
defer s.mu.Unlock()
153+
154+
if s.closed {
155+
return errors.New("session is closed")
156+
}
157+
158+
// ArgoCD terminal uses JSON messages
159+
msg, err := json.Marshal(terminalMessage{
160+
Operation: "resize",
161+
Cols: cols,
162+
Rows: rows,
163+
})
164+
if err != nil {
165+
return err
166+
}
167+
return s.wsConn.WriteMessage(websocket.TextMessage, msg)
168+
}
169+
170+
// GetOutput returns all captured output so far
171+
func (s *TerminalClient) GetOutput() string {
172+
s.outputMu.Lock()
173+
defer s.outputMu.Unlock()
174+
return s.output.String()
175+
}
176+
177+
// WaitForOutput waits until the output contains the expected string or timeout
178+
func (s *TerminalClient) WaitForOutput(expected string, timeout time.Duration) bool {
179+
deadline := time.Now().Add(timeout)
180+
for time.Now().Before(deadline) {
181+
if strings.Contains(s.GetOutput(), expected) {
182+
return true
183+
}
184+
time.Sleep(100 * time.Millisecond)
185+
}
186+
return false
187+
}
188+
189+
// Close closes the terminal session
190+
func (s *TerminalClient) Close() error {
191+
s.mu.Lock()
192+
defer s.mu.Unlock()
193+
194+
if s.closed {
195+
return nil
196+
}
197+
s.closed = true
198+
return s.wsConn.Close()
199+
}

test/openshift/e2e/ginkgo/fixture/clusterserviceversion/fixture.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package clusterserviceversion
22

33
import (
44
"context"
5+
"strings"
56

67
. "github.com/onsi/gomega"
78
olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
@@ -27,5 +28,16 @@ func Update(obj *olmv1alpha1.ClusterServiceVersion, modify func(*olmv1alpha1.Clu
2728
return k8sClient.Update(context.Background(), obj)
2829
})
2930
Expect(err).ToNot(HaveOccurred())
31+
}
3032

33+
func Get(ctx context.Context, k8sClient client.Client) *olmv1alpha1.ClusterServiceVersion {
34+
var csvList olmv1alpha1.ClusterServiceVersionList
35+
Expect(k8sClient.List(ctx, &csvList, client.InNamespace("openshift-gitops-operator"))).To(Succeed())
36+
for idx := range csvList.Items {
37+
idxCSV := csvList.Items[idx]
38+
if strings.Contains(idxCSV.Name, "gitops-operator") {
39+
return &idxCSV
40+
}
41+
}
42+
return nil
3143
}

test/openshift/e2e/ginkgo/fixture/utils/fixtureUtils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils
33
import (
44
"os"
55

6+
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
67
"k8s.io/apimachinery/pkg/runtime"
78
"k8s.io/client-go/rest"
89
"k8s.io/client-go/tools/clientcmd"
@@ -146,6 +147,10 @@ func getKubeClient(config *rest.Config) (client.Client, *runtime.Scheme, error)
146147
return nil, nil, err
147148
}
148149

150+
if err := certificatesv1beta1.AddToScheme(scheme); err != nil {
151+
return nil, nil, err
152+
}
153+
149154
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
150155
if err != nil {
151156
return nil, nil, err

test/openshift/e2e/ginkgo/sequential/1-053_validate_argocd_agent_principal_connected_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() {
516516
It("Should deploy ArgoCD principal and agent instances in both modes and verify they are working as expected", func() {
517517

518518
By("Deploy principal and verify it starts successfully")
519-
deployPrincipal(ctx, k8sClient, registerCleanup)
519+
deployPrincipal(ctx, k8sClient, registerCleanup, false)
520520

521521
By("Deploy managed agent and verify it starts successfully")
522522
deployAgent(ctx, k8sClient, registerCleanup, argov1beta1api.AgentModeManaged)
@@ -609,7 +609,7 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() {
609609
// This function deploys the principal ArgoCD instance and waits for it to be ready.
610610
// It creates the required secrets for the principal and verifies that the principal deployment is in Ready state.
611611
// It also verifies that the principal logs contain the expected messages.
612-
func deployPrincipal(ctx context.Context, k8sClient client.Client, registerCleanup func(func())) {
612+
func deployPrincipal(ctx context.Context, k8sClient client.Client, registerCleanup func(func()), enableServerRoute bool) {
613613
GinkgoHelper()
614614

615615
nsPrincipal, cleanup := fixture.CreateNamespaceWithCleanupFunc(namespaceAgentPrincipal)
@@ -624,6 +624,12 @@ func deployPrincipal(ctx context.Context, k8sClient client.Client, registerClean
624624
waitForLoadBalancer = false
625625
}
626626

627+
if enableServerRoute {
628+
argoCDInstance.Spec.Server.Route = argov1beta1api.ArgoCDRouteSpec{
629+
Enabled: true,
630+
}
631+
}
632+
627633
Expect(k8sClient.Create(ctx, argoCDInstance)).To(Succeed())
628634

629635
By("Wait for principal service to be ready and use LoadBalancer hostname/IP when available")
@@ -678,7 +684,7 @@ func deployPrincipal(ctx context.Context, k8sClient client.Client, registerClean
678684

679685
Eventually(&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
680686
Name: deploymentNameAgentPrincipal,
681-
Namespace: nsPrincipal.Name}}, "120s", "5s").Should(deploymentFixture.HaveReadyReplicas(1))
687+
Namespace: nsPrincipal.Name}}, "240s", "5s").Should(deploymentFixture.HaveReadyReplicas(1))
682688

683689
By("Verify principal logs contain expected messages")
684690

0 commit comments

Comments
 (0)