|
| 1 | +package quincey |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "strconv" |
| 6 | + |
| 7 | + "github.com/init4tech/signet-infra-components/pkg/utils" |
| 8 | + crd "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" |
| 9 | + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" |
| 10 | + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" |
| 11 | + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" |
| 12 | + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" |
| 13 | +) |
| 14 | + |
| 15 | +// NewQuinceyComponent creates a new Quincey deployment in the specified namespace. |
| 16 | +// It sets up all necessary Kubernetes resources including the deployment, service, |
| 17 | +// and Istio configurations. |
| 18 | +// |
| 19 | +// Example: |
| 20 | +// |
| 21 | +// component, err := NewQuinceyComponent(ctx, "quincey", &QuinceyComponentArgs{ |
| 22 | +// Namespace: pulumi.String("default"), |
| 23 | +// Image: pulumi.String("quincey:latest"), |
| 24 | +// Env: QuinceyEnv{ |
| 25 | +// QuinceyPort: pulumi.String("8080"), |
| 26 | +// // ... other environment variables |
| 27 | +// }, |
| 28 | +// }) |
| 29 | +func NewQuinceyComponent(ctx *pulumi.Context, name string, args *QuinceyComponentArgs, opts ...pulumi.ResourceOption) (*QuinceyComponent, error) { |
| 30 | + if err := args.Validate(); err != nil { |
| 31 | + return nil, fmt.Errorf("invalid quincey component args: %w", err) |
| 32 | + } |
| 33 | + |
| 34 | + component := &QuinceyComponent{ |
| 35 | + ResourceState: pulumi.ResourceState{}, |
| 36 | + } |
| 37 | + |
| 38 | + if err := ctx.RegisterComponentResource("signet:index:Quincey", name, component); err != nil { |
| 39 | + return nil, fmt.Errorf("failed to register component resource: %w", err) |
| 40 | + } |
| 41 | + |
| 42 | + // Create service account |
| 43 | + serviceAccount, err := createServiceAccount(ctx, args.Namespace, component) |
| 44 | + if err != nil { |
| 45 | + return nil, fmt.Errorf("failed to create service account: %w", err) |
| 46 | + } |
| 47 | + |
| 48 | + // Create config map |
| 49 | + configMap, err := createConfigMap(ctx, args, component) |
| 50 | + if err != nil { |
| 51 | + return nil, fmt.Errorf("failed to create config map: %w", err) |
| 52 | + } |
| 53 | + |
| 54 | + // Create deployment |
| 55 | + deployment, err := createDeployment(ctx, args, component) |
| 56 | + if err != nil { |
| 57 | + return nil, fmt.Errorf("failed to create deployment: %w", err) |
| 58 | + } |
| 59 | + |
| 60 | + // Create service |
| 61 | + service, err := createService(ctx, args, deployment, component) |
| 62 | + if err != nil { |
| 63 | + return nil, fmt.Errorf("failed to create service: %w", err) |
| 64 | + } |
| 65 | + |
| 66 | + // Create virtual service |
| 67 | + virtualService, err := createVirtualService(ctx, args, service, component) |
| 68 | + if err != nil { |
| 69 | + return nil, fmt.Errorf("failed to create virtual service: %w", err) |
| 70 | + } |
| 71 | + |
| 72 | + // Create request authentication |
| 73 | + requestAuth, err := createRequestAuthentication(ctx, args, component) |
| 74 | + if err != nil { |
| 75 | + return nil, fmt.Errorf("failed to create request authentication: %w", err) |
| 76 | + } |
| 77 | + |
| 78 | + // Create authorization policy |
| 79 | + authPolicy, err := createAuthorizationPolicy(ctx, component) |
| 80 | + if err != nil { |
| 81 | + return nil, fmt.Errorf("failed to create authorization policy: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + component.Service = service |
| 85 | + component.ServiceAccount = serviceAccount |
| 86 | + component.ConfigMap = configMap |
| 87 | + component.Deployment = deployment |
| 88 | + component.VirtualService = virtualService |
| 89 | + component.RequestAuthentication = requestAuth |
| 90 | + component.AuthorizationPolicy = authPolicy |
| 91 | + |
| 92 | + return component, nil |
| 93 | +} |
| 94 | + |
| 95 | +// createServiceAccount creates a Kubernetes service account for the Quincey service |
| 96 | +func createServiceAccount(ctx *pulumi.Context, namespace pulumi.StringInput, parent *QuinceyComponent) (*corev1.ServiceAccount, error) { |
| 97 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 98 | + |
| 99 | + return corev1.NewServiceAccount(ctx, ServiceName, &corev1.ServiceAccountArgs{ |
| 100 | + Metadata: &metav1.ObjectMetaArgs{ |
| 101 | + Name: pulumi.String(ServiceName), |
| 102 | + Namespace: namespace, |
| 103 | + Labels: labels, |
| 104 | + }, |
| 105 | + }, pulumi.Parent(parent)) |
| 106 | +} |
| 107 | + |
| 108 | +// createDeployment creates the Kubernetes deployment for the Quincey service |
| 109 | +func createDeployment(ctx *pulumi.Context, args *QuinceyComponentArgs, parent *QuinceyComponent) (*appsv1.Deployment, error) { |
| 110 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 111 | + |
| 112 | + containerPortInt := args.Env.QuinceyPort.ToStringOutput().ApplyT(func(s string) int { |
| 113 | + port, _ := strconv.Atoi(s) |
| 114 | + return port |
| 115 | + }).(pulumi.IntOutput) |
| 116 | + |
| 117 | + return appsv1.NewDeployment(ctx, ServiceName, &appsv1.DeploymentArgs{ |
| 118 | + Metadata: &metav1.ObjectMetaArgs{ |
| 119 | + Name: pulumi.String(ServiceName), |
| 120 | + Namespace: args.Namespace, |
| 121 | + Labels: labels, |
| 122 | + }, |
| 123 | + Spec: &appsv1.DeploymentSpecArgs{ |
| 124 | + Selector: &metav1.LabelSelectorArgs{ |
| 125 | + MatchLabels: labels, |
| 126 | + }, |
| 127 | + Replicas: pulumi.Int(1), |
| 128 | + Template: &corev1.PodTemplateSpecArgs{ |
| 129 | + Metadata: &metav1.ObjectMetaArgs{ |
| 130 | + Labels: labels, |
| 131 | + }, |
| 132 | + Spec: &corev1.PodSpecArgs{ |
| 133 | + ServiceAccountName: pulumi.String(ServiceName), |
| 134 | + Containers: corev1.ContainerArray{ |
| 135 | + createContainer(args, containerPortInt), |
| 136 | + }, |
| 137 | + }, |
| 138 | + }, |
| 139 | + }, |
| 140 | + }, pulumi.Parent(parent)) |
| 141 | +} |
| 142 | + |
| 143 | +// createConfigMap creates the ConfigMap for the Quincey service |
| 144 | +func createConfigMap(ctx *pulumi.Context, args *QuinceyComponentArgs, parent *QuinceyComponent) (*corev1.ConfigMap, error) { |
| 145 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 146 | + |
| 147 | + return utils.CreateConfigMap(ctx, ServiceName, args.Namespace, labels, args.Env) |
| 148 | +} |
| 149 | + |
| 150 | +// createContainer creates the container specification for the Quincey service |
| 151 | +func createContainer(args *QuinceyComponentArgs, port pulumi.IntOutput) *corev1.ContainerArgs { |
| 152 | + return &corev1.ContainerArgs{ |
| 153 | + Name: pulumi.String(ServiceName), |
| 154 | + Image: args.Image, |
| 155 | + EnvFrom: corev1.EnvFromSourceArray{ |
| 156 | + &corev1.EnvFromSourceArgs{ |
| 157 | + ConfigMapRef: &corev1.ConfigMapEnvSourceArgs{ |
| 158 | + Name: pulumi.String(ServiceName), |
| 159 | + }, |
| 160 | + }, |
| 161 | + }, |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +// createService creates the Kubernetes service for the Quincey service |
| 166 | +func createService(ctx *pulumi.Context, args *QuinceyComponentArgs, deployment *appsv1.Deployment, parent *QuinceyComponent) (*corev1.Service, error) { |
| 167 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 168 | + |
| 169 | + containerPortInt := args.Env.QuinceyPort.ToStringOutput().ApplyT(func(s string) int { |
| 170 | + port, err := strconv.Atoi(s) |
| 171 | + if err != nil { |
| 172 | + fmt.Printf("Error converting QuinceyPort to integer: %v. Using default port 8080.\n", err) |
| 173 | + return 8080 |
| 174 | + } |
| 175 | + return port |
| 176 | + }).(pulumi.IntOutput) |
| 177 | + |
| 178 | + return corev1.NewService(ctx, "quincey-server-service", &corev1.ServiceArgs{ |
| 179 | + Metadata: &metav1.ObjectMetaArgs{ |
| 180 | + Name: pulumi.String("quincey"), |
| 181 | + Namespace: args.Namespace, |
| 182 | + Labels: labels, |
| 183 | + }, |
| 184 | + Spec: &corev1.ServiceSpecArgs{ |
| 185 | + Selector: labels, |
| 186 | + Ports: corev1.ServicePortArray{ |
| 187 | + &corev1.ServicePortArgs{ |
| 188 | + Port: containerPortInt, |
| 189 | + TargetPort: containerPortInt, |
| 190 | + }, |
| 191 | + }, |
| 192 | + Type: pulumi.String("ClusterIP"), |
| 193 | + }, |
| 194 | + }, pulumi.DependsOn([]pulumi.Resource{deployment}), pulumi.Parent(parent)) |
| 195 | +} |
| 196 | + |
| 197 | +// createVirtualService creates the Istio virtual service for the Quincey service |
| 198 | +func createVirtualService(ctx *pulumi.Context, args *QuinceyComponentArgs, service *corev1.Service, parent *QuinceyComponent) (*crd.CustomResource, error) { |
| 199 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 200 | + |
| 201 | + containerPortInt := args.Env.QuinceyPort.ToStringOutput().ApplyT(func(s string) int { |
| 202 | + port, err := strconv.Atoi(s) |
| 203 | + if err != nil { |
| 204 | + fmt.Printf("Error converting QuinceyPort to integer: %v. Using default port 8080.\n", err) |
| 205 | + return 8080 |
| 206 | + } |
| 207 | + return port |
| 208 | + }).(pulumi.IntOutput) |
| 209 | + |
| 210 | + // Get the service URL using the existing method |
| 211 | + serviceURL := parent.GetServiceURL() |
| 212 | + |
| 213 | + return crd.NewCustomResource(ctx, "quincey-vservice", &crd.CustomResourceArgs{ |
| 214 | + ApiVersion: pulumi.String("networking.istio.io/v1alpha3"), |
| 215 | + Kind: pulumi.String("VirtualService"), |
| 216 | + Metadata: &metav1.ObjectMetaArgs{ |
| 217 | + Name: pulumi.String("quincey"), |
| 218 | + Namespace: args.Namespace, |
| 219 | + Labels: labels, |
| 220 | + }, |
| 221 | + OtherFields: map[string]interface{}{ |
| 222 | + "spec": map[string]interface{}{ |
| 223 | + "hosts": args.VirtualServiceHosts, |
| 224 | + "gateways": []string{ |
| 225 | + "default/init4-api-gateway", |
| 226 | + }, |
| 227 | + "http": []map[string]interface{}{ |
| 228 | + { |
| 229 | + "match": []map[string]interface{}{ |
| 230 | + { |
| 231 | + "uri": map[string]interface{}{ |
| 232 | + "prefix": "/signBlock", |
| 233 | + }, |
| 234 | + }, |
| 235 | + { |
| 236 | + "uri": map[string]interface{}{ |
| 237 | + "prefix": "/healthCheck", |
| 238 | + }, |
| 239 | + }, |
| 240 | + }, |
| 241 | + "route": []map[string]interface{}{ |
| 242 | + { |
| 243 | + "destination": map[string]interface{}{ |
| 244 | + "host": serviceURL, |
| 245 | + "port": map[string]interface{}{ |
| 246 | + "number": containerPortInt, |
| 247 | + }, |
| 248 | + }, |
| 249 | + }, |
| 250 | + }, |
| 251 | + }, |
| 252 | + }, |
| 253 | + }, |
| 254 | + }, |
| 255 | + }, pulumi.DependsOn([]pulumi.Resource{service}), pulumi.Parent(parent)) |
| 256 | +} |
| 257 | + |
| 258 | +// createRequestAuthentication creates the Istio request authentication policy |
| 259 | +func createRequestAuthentication(ctx *pulumi.Context, args *QuinceyComponentArgs, parent *QuinceyComponent) (*crd.CustomResource, error) { |
| 260 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 261 | + |
| 262 | + return crd.NewCustomResource(ctx, "quincey-authorization-policy", &crd.CustomResourceArgs{ |
| 263 | + ApiVersion: pulumi.String("security.istio.io/v1beta1"), |
| 264 | + Kind: pulumi.String("RequestAuthentication"), |
| 265 | + Metadata: &metav1.ObjectMetaArgs{ |
| 266 | + Name: pulumi.String("quincey-jwt-policy"), |
| 267 | + Namespace: args.Namespace, |
| 268 | + Labels: labels, |
| 269 | + }, |
| 270 | + OtherFields: map[string]interface{}{ |
| 271 | + "spec": map[string]interface{}{ |
| 272 | + "selector": map[string]interface{}{ |
| 273 | + "matchLabels": labels, |
| 274 | + }, |
| 275 | + "jwtRules": []map[string]interface{}{ |
| 276 | + { |
| 277 | + "issuer": args.Env.OauthIssuer, |
| 278 | + "jwksUri": args.Env.OauthJwksUri, |
| 279 | + "outputClaimToHeaders": []map[string]interface{}{ |
| 280 | + { |
| 281 | + "claim": "sub", |
| 282 | + "header": "x-jwt-claim-sub", |
| 283 | + }, |
| 284 | + }, |
| 285 | + }, |
| 286 | + }, |
| 287 | + }, |
| 288 | + }, |
| 289 | + }, pulumi.Parent(parent)) |
| 290 | +} |
| 291 | + |
| 292 | +// createAuthorizationPolicy creates the Istio authorization policy |
| 293 | +func createAuthorizationPolicy(ctx *pulumi.Context, parent *QuinceyComponent) (*crd.CustomResource, error) { |
| 294 | + labels := utils.CreateResourceLabels(ComponentName, ServiceName, "signet", nil) |
| 295 | + |
| 296 | + return crd.NewCustomResource(ctx, "quincey-authorization-policy", &crd.CustomResourceArgs{ |
| 297 | + ApiVersion: pulumi.String("security.istio.io/v1beta1"), |
| 298 | + Kind: pulumi.String("AuthorizationPolicy"), |
| 299 | + Metadata: &metav1.ObjectMetaArgs{ |
| 300 | + Name: pulumi.String("quincey-jwt-auth-policy"), |
| 301 | + Namespace: parent.Service.Metadata.Namespace(), |
| 302 | + Labels: labels, |
| 303 | + }, |
| 304 | + OtherFields: map[string]interface{}{ |
| 305 | + "spec": map[string]interface{}{ |
| 306 | + "selector": map[string]interface{}{ |
| 307 | + "matchLabels": labels, |
| 308 | + }, |
| 309 | + "action": "ALLOW", |
| 310 | + "rules": []map[string]interface{}{ |
| 311 | + { |
| 312 | + "from": []map[string]interface{}{ |
| 313 | + { |
| 314 | + "source": map[string]interface{}{ |
| 315 | + "requestPrincipals": []string{ |
| 316 | + "*", |
| 317 | + }, |
| 318 | + }, |
| 319 | + }, |
| 320 | + }, |
| 321 | + }, |
| 322 | + }, |
| 323 | + }, |
| 324 | + }, |
| 325 | + }, pulumi.Parent(parent)) |
| 326 | +} |
| 327 | + |
| 328 | +// GetServiceURL returns the URL of the builder service |
| 329 | +func (c *QuinceyComponent) GetServiceURL() pulumi.StringOutput { |
| 330 | + return pulumi.Sprintf("http://%s.%s.svc.cluster.local", c.Service.Metadata.Name(), c.Service.Metadata.Namespace()) |
| 331 | +} |
| 332 | + |
| 333 | +// GetMetricsURL returns the URL of the builder metrics endpoint |
| 334 | +func (c *QuinceyComponent) GetMetricsURL() pulumi.StringOutput { |
| 335 | + return pulumi.Sprintf("http://%s.%s.svc.cluster.local:%d/metrics", |
| 336 | + c.Service.Metadata.Name(), |
| 337 | + c.Service.Metadata.Namespace(), |
| 338 | + DefaultMetricsPort) |
| 339 | +} |
0 commit comments