/
session.go
292 lines (253 loc) · 8.97 KB
/
session.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package session
import (
"os/user"
"regexp"
"strings"
"time"
"emperror.dev/errors"
"github.com/go-logr/logr"
istiov1alpha1 "github.com/maistra/istio-workspace/api/maistra/v1alpha1"
"github.com/maistra/istio-workspace/pkg/k8s"
"github.com/maistra/istio-workspace/pkg/log"
"github.com/maistra/istio-workspace/pkg/naming"
"github.com/maistra/istio-workspace/pkg/openshift"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/wait"
)
var (
logger = func() logr.Logger {
return log.Log.WithValues("type", "session")
}
errorWrongRouteFormat = errors.Sentinel("route in wrong format. expected type:name=value")
)
// Options holds the variables used by the Session Handler.
type Options struct {
NamespaceName string // name of the namespace for target resource
DeploymentName string // name of the initial resource to target
SessionName string // name of the session create or join if exist
RouteExp string // expression of how to route the traffic to the target resource
Strategy string // name of the strategy to use for the target resource
StrategyArgs map[string]string // additional arguments for the strategy
Revert bool // Revert back to previous known value if join/leave a existing session with a known ref
Duration *time.Duration // Duration defines the interval used to check for changes to the session object
}
// State holds the new variables as presented by the creation of the session.
type State struct {
DeploymentName string // name of the resource to target within the cloned route.
Hosts []string // currently exposed hosts
Route istiov1alpha1.Route // the current route configuration
}
// Handler is a function to setup a server session before attempting to connect. Returns a 'cleanup' function.
type Handler func(opts Options, client *Client) (State, func(), error)
// Offline is a empty Handler doing nothing. Used for testing.
func Offline(opts Options, client *Client) (State, func(), error) {
return State{DeploymentName: opts.DeploymentName}, func() {}, nil
}
// handler wraps the session client and required metadata used to manipulate the resources.
type handler struct {
c *Client
opts Options
previousState *istiov1alpha1.Ref // holds the previous Ref if replaced. Used to Revert back to old state on remove.
}
// RemoveHandler provides the option to delete an existing sessions if found.
// Rely on the following flags:
// - namespace - the name of the target namespace where deployment is defined
// - session - the name of the session.
func RemoveHandler(opts Options, client *Client) (State, func()) { //nolint:gocritic //reason too simple to use named results
if client == nil {
return State{}, func() {}
}
h := &handler{c: client,
opts: opts}
return State{}, func() {
h.removeOrLeaveSession()
}
}
// CreateOrJoinHandler provides the option to either create a new session if non exist or join an existing.
// Rely on the following flags:
// - namespace - the name of the target namespace where deployment is defined
// - deployment - the name of the target deployment and will update the flag with the new deployment name
// - session - the name of the session
// - route - the definition of traffic routing.
func CreateOrJoinHandler(opts Options, client *Client) (State, func(), error) {
sessionName, err := getOrCreateSessionName(opts.SessionName)
if err != nil {
return State{}, func() {}, err
}
opts.SessionName = sessionName
h := &handler{c: client, opts: opts}
session, serviceName, err := h.createOrJoinSession()
if err != nil {
return State{}, h.removeOrLeaveSession, err
}
route := session.Status.Route
if route == nil {
route = &istiov1alpha1.Route{}
}
return State{
DeploymentName: serviceName,
Hosts: session.Status.Hosts,
Route: *route,
}, h.removeOrLeaveSession, nil
}
func (h *handler) createSession() (*istiov1alpha1.Session, error) {
r, err := ParseRoute(h.opts.RouteExp)
if err != nil {
return nil, err
}
session := istiov1alpha1.Session{
TypeMeta: metav1.TypeMeta{
APIVersion: "workspace.maistra.io/v1alpha1",
Kind: "Session",
},
ObjectMeta: metav1.ObjectMeta{
Name: h.opts.SessionName,
},
Spec: istiov1alpha1.SessionSpec{
Refs: []istiov1alpha1.Ref{
{Name: h.opts.DeploymentName, Strategy: h.opts.Strategy, Args: h.opts.StrategyArgs},
},
},
}
if r != nil {
session.Spec.Route = *r
}
return &session, h.c.Create(&session)
}
// createOrJoinSession calls oc cli and creates a Session CD waiting for the 'success' status and return the new name.
func (h *handler) createOrJoinSession() (*istiov1alpha1.Session, string, error) {
session, err := h.c.Get(h.opts.SessionName)
if err != nil {
session, err = h.createSession()
if err != nil {
return session, "", err
}
return h.waitForRefToComplete()
}
ref := istiov1alpha1.Ref{Name: h.opts.DeploymentName, Strategy: h.opts.Strategy, Args: h.opts.StrategyArgs}
// update ref in session
for i, r := range session.Spec.Refs {
if r.Name != h.opts.DeploymentName {
continue
}
prev := session.Spec.Refs[i]
h.previousState = &prev // point to a variable, not a array index
session.Spec.Refs[i] = ref
err = h.c.Update(session)
if err != nil {
return session, "", err
}
return h.waitForRefToComplete()
}
// join session
session.Spec.Refs = append(session.Spec.Refs, ref)
err = h.c.Update(session)
if err != nil {
return session, "", err
}
return h.waitForRefToComplete()
}
func (h *handler) waitForRefToComplete() (*istiov1alpha1.Session, string, error) {
var err error
var sessionStatus *istiov1alpha1.Session
duration := 1 * time.Minute
if h.opts.Duration != nil {
duration = *h.opts.Duration
}
err = wait.Poll(2*time.Second, duration, func() (bool, error) {
sessionStatus, err = h.c.Get(h.opts.SessionName)
if err != nil {
return false, err
}
if sessionStatus.Status.State != nil && *sessionStatus.Status.State == istiov1alpha1.StateSuccess {
return true, nil
}
return false, nil
})
if err != nil {
return sessionStatus, "", errors.Wrap(err, "timed out waiting for success")
}
for _, condition := range sessionStatus.Status.Conditions {
if condition.Target != nil && refMatchesDeploymentName(condition, h.opts.DeploymentName) && deploymentOrDeploymentConfig(condition) && notDeleted(condition) {
return sessionStatus, condition.Target.Name, nil
}
}
return sessionStatus, "", DeploymentNotFoundError{name: h.opts.DeploymentName}
}
func notDeleted(condition *istiov1alpha1.Condition) bool {
return condition.Type != nil && *condition.Type != "delete"
}
func deploymentOrDeploymentConfig(condition *istiov1alpha1.Condition) bool {
return condition.Source.Kind == k8s.DeploymentKind || condition.Source.Kind == openshift.DeploymentConfigKind
}
func refMatchesDeploymentName(condition *istiov1alpha1.Condition, name string) bool {
return condition.Source.Ref == name
}
func (h *handler) removeOrLeaveSession() {
session, err := h.c.Get(h.opts.SessionName)
if err != nil {
logger().Error(err, "failed removing or leaving session")
return // assume missing, nothing to clean?
}
// more than one participant, update session
for i, r := range session.Spec.Refs {
if r.Name == h.opts.DeploymentName {
if h.opts.Revert && h.previousState != nil {
session.Spec.Refs[i] = *h.previousState
} else {
session.Spec.Refs = append(session.Spec.Refs[:i], session.Spec.Refs[i+1:]...)
}
}
}
if len(session.Spec.Refs) == 0 {
_ = h.c.Delete(session)
} else {
_ = h.c.Update(session)
}
}
var nonAlphaNumeric = regexp.MustCompile("[^A-Za-z0-9]+")
func getOrCreateSessionName(sessionName string) (string, error) {
if sessionName != "" {
namingErrors := validation.IsDNS1123Label(sessionName)
if len(namingErrors) != 0 {
var errs []error
for _, namingError := range namingErrors {
errs = append(errs, errors.New(namingError))
}
return "", errors.WrapIfWithDetails(errors.Combine(errs...), "your suggested session name is not a valid k8s value", "name", sessionName)
}
}
if sessionName == "" {
random := naming.RandName(5)
u, err := user.Current()
if err != nil {
sessionName = random
} else {
sessionName = naming.ConcatToMax(63, "user", u.Username, random)
}
}
return nonAlphaNumeric.ReplaceAllString(sessionName, "-"), nil
}
// ParseRoute maps string route representation into a Route struct by unwrapping its type, name and value.
func ParseRoute(route string) (*istiov1alpha1.Route, error) {
if route == "" {
return nil, nil //nolint:nilnil //reason empty route will be generated in the controller
}
var t, n, v string
typed := strings.Split(route, ":")
if len(typed) != 2 {
return nil, errorWrongRouteFormat
}
t = typed[0]
pair := strings.Split(typed[1], "=")
if len(pair) != 2 {
return nil, errorWrongRouteFormat
}
n, v = pair[0], pair[1]
return &istiov1alpha1.Route{
Type: t,
Name: n,
Value: v,
}, nil
}