github.com/docker/compose-on-kubernetes@v0.5.0/install/installer.go (about) 1 package install 2 3 import ( 4 "context" 5 "crypto/sha1" 6 "encoding/hex" 7 "fmt" 8 "time" 9 10 log "github.com/sirupsen/logrus" 11 corev1types "k8s.io/api/core/v1" 12 apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" 13 apierrors "k8s.io/apimachinery/pkg/api/errors" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/apimachinery/pkg/runtime" 16 "k8s.io/client-go/discovery" 17 appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1" 18 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 19 rbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" 20 "k8s.io/client-go/rest" 21 kubeaggreagatorv1beta1 "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1" 22 ) 23 24 type installer struct { 25 coreClient corev1.CoreV1Interface 26 rbacClient rbacv1.RbacV1Interface 27 appsClient appsv1.AppsV1Interface 28 aggregatorClient kubeaggreagatorv1beta1.ApiregistrationV1beta1Interface 29 commonOptions OptionsCommon 30 etcdOptions *EtcdOptions 31 networkOptions *NetworkOptions 32 enableCoverage bool 33 config *rest.Config 34 labels map[string]string 35 apiLabels map[string]string 36 disableController bool 37 controllerOnly bool 38 controllerImageOverride string 39 apiServerImageOverride string 40 objectFilter runtimeObjectFilters 41 customMatch func(Status) bool 42 expiresOffset time.Duration 43 customTLSHash string 44 debugImages bool 45 } 46 47 // RuntimeObjectFilter allows to modify or bypass completely a k8s object 48 type RuntimeObjectFilter func(runtime.Object) (bool, error) 49 50 type runtimeObjectFilters []RuntimeObjectFilter 51 52 func (fs runtimeObjectFilters) filter(obj runtime.Object) (bool, error) { 53 for _, f := range fs { 54 if res, err := f(obj); err != nil || !res { 55 return res, err 56 } 57 } 58 return true, nil 59 } 60 61 type installerContext struct { 62 pullSecret *corev1types.Secret 63 serviceAccount *corev1types.ServiceAccount 64 } 65 66 // Do proceeds with installing 67 func Do(ctx context.Context, config *rest.Config, options ...InstallerOption) error { 68 installer, err := newInstaller(config, options...) 69 if err != nil { 70 return err 71 } 72 return installer.install(ctx) 73 } 74 75 // InstallerOption defines modifies the installer 76 type InstallerOption func(*installer) 77 78 // WithObjectFilter applies a RuntimeObjectFilter 79 func WithObjectFilter(filter RuntimeObjectFilter) InstallerOption { 80 return func(i *installer) { 81 i.objectFilter = append(i.objectFilter, filter) 82 } 83 } 84 85 // WithExpiresOffset specifies the duration offset to apply when checking if generated tls bundle has expired 86 func WithExpiresOffset(d time.Duration) InstallerOption { 87 return func(i *installer) { 88 i.expiresOffset = d 89 } 90 } 91 92 // WithoutController install components without the controller 93 func WithoutController() InstallerOption { 94 return func(i *installer) { 95 i.disableController = true 96 } 97 } 98 99 // WithControllerImage overrides controller image selection 100 func WithControllerImage(image string) InstallerOption { 101 return func(i *installer) { 102 i.controllerImageOverride = image 103 } 104 } 105 106 // WithAPIServerImage overrides API server image selection 107 func WithAPIServerImage(image string) InstallerOption { 108 return func(i *installer) { 109 i.apiServerImageOverride = image 110 } 111 } 112 113 // WithControllerOnly installs only the controller 114 func WithControllerOnly() InstallerOption { 115 return func(i *installer) { 116 i.controllerOnly = true 117 } 118 } 119 120 // WithUnsafe initializes the installer with unsafe options 121 func WithUnsafe(o UnsafeOptions) InstallerOption { 122 return func(i *installer) { 123 i.commonOptions = o.OptionsCommon 124 i.enableCoverage = o.Coverage 125 i.debugImages = o.Debug 126 } 127 } 128 129 // WithSafe initializes the installer with Safe options 130 func WithSafe(o SafeOptions) InstallerOption { 131 return func(i *installer) { 132 i.commonOptions = o.OptionsCommon 133 i.etcdOptions = &o.Etcd 134 i.networkOptions = &o.Network 135 } 136 } 137 138 // WithCustomStatusMatch allows to provide additional predicates to 139 // check if the current install status matches the desired state 140 func WithCustomStatusMatch(match func(Status) bool) InstallerOption { 141 return func(i *installer) { 142 i.customMatch = match 143 } 144 } 145 146 func tagForCustomImages(controllerImage, apiServerImage string) string { 147 bytes := sha1.Sum([]byte(fmt.Sprintf("%s %s", controllerImage, apiServerImage))) 148 return hex.EncodeToString(bytes[:]) 149 } 150 151 func newInstaller(config *rest.Config, options ...InstallerOption) (*installer, error) { 152 i := &installer{ 153 config: config, 154 } 155 // default expires offset is 30 days 156 i.expiresOffset = 30 * 24 * time.Hour 157 for _, o := range options { 158 o(i) 159 } 160 if i.controllerImageOverride != "" && i.apiServerImageOverride != "" { 161 // compute a tag for these images 162 i.commonOptions.Tag = tagForCustomImages(i.controllerImageOverride, i.apiServerImageOverride) 163 } 164 if i.debugImages { 165 i.commonOptions.Tag = "debug" 166 } 167 i.labels = map[string]string{ 168 fryKey: composeFry, 169 imageTagKey: i.commonOptions.Tag, 170 namespaceKey: i.commonOptions.Namespace, 171 defaultServiceTypeKey: i.commonOptions.DefaultServiceType, 172 } 173 i.apiLabels = map[string]string{ 174 fryKey: composeAPIServerFry, 175 imageTagKey: i.commonOptions.Tag, 176 namespaceKey: i.commonOptions.Namespace, 177 } 178 if i.networkOptions != nil && 179 i.networkOptions.CustomTLSBundle != nil { 180 // hash tls data so that we can use that to dertermine if we need to re-deploy 181 dataToHash := append(append(i.networkOptions.CustomTLSBundle.ca, i.networkOptions.CustomTLSBundle.cert...), i.networkOptions.CustomTLSBundle.key...) 182 hash := sha1.Sum(dataToHash) 183 i.customTLSHash = hex.EncodeToString(hash[:]) 184 } 185 coreClient, err := corev1.NewForConfig(config) 186 if err != nil { 187 return nil, err 188 } 189 i.coreClient = coreClient 190 191 rbacClient, err := rbacv1.NewForConfig(config) 192 if err != nil { 193 return nil, err 194 } 195 i.rbacClient = rbacClient 196 197 appsClient, err := appsv1.NewForConfig(config) 198 if err != nil { 199 return nil, err 200 } 201 i.appsClient = appsClient 202 203 aggregatorClient, err := kubeaggreagatorv1beta1.NewForConfig(config) 204 if err != nil { 205 return nil, err 206 } 207 i.aggregatorClient = aggregatorClient 208 209 client, err := discovery.NewDiscoveryClientForConfig(config) 210 if err != nil { 211 return nil, err 212 } 213 214 kubeVersion, err := client.ServerVersion() 215 if err != nil { 216 return nil, err 217 } 218 219 return i, checkVersion(kubeVersion, minimumServerVersion) 220 } 221 222 // Status reports current installation status details 223 type Status struct { 224 // True if there is a deployment with compose labels in the cluster 225 IsInstalled bool 226 // Tag of the installed components 227 Tag string 228 // Indicates if there is a legacy compose CRD in the system 229 IsCrdPresent bool 230 // Namespace in which components are deployed 231 Namespace string 232 // Image of the controller 233 ControllerImage string 234 // Image of the API service 235 APIServiceImage string 236 // Default service type for published services 237 DefaultServiceType string 238 // ControllerLabels contains all labels from Controller deployment 239 ControllerLabels map[string]string 240 // APIServiceLabels contains all labels from API service deployment 241 APIServiceLabels map[string]string 242 // APIServiceAnnotations contains annotations from the API service deployment 243 APIServiceAnnotations map[string]string 244 } 245 246 func (c *installer) isInstalled() (Status, error) { 247 crds, err := apiextensionsclient.NewForConfig(c.config) 248 if err != nil { 249 return Status{}, err 250 } 251 isCrdPresent := true 252 _, err = crds.CustomResourceDefinitions().Get("stacks.compose.docker.com", metav1.GetOptions{}) 253 if err != nil { 254 if apierrors.IsNotFound(err) { 255 isCrdPresent = false 256 } else { 257 return Status{}, err 258 } 259 } 260 apps, err := c.appsClient.Deployments(metav1.NamespaceAll).List(metav1.ListOptions{ 261 LabelSelector: everythingSelector, 262 }) 263 if err != nil { 264 return Status{}, err 265 } 266 if len(apps.Items) == 0 { 267 return Status{ 268 IsInstalled: false, 269 IsCrdPresent: isCrdPresent, 270 }, nil 271 } 272 tag := "" 273 if apps.Items[0].Labels != nil { 274 tag = apps.Items[0].Labels[imageTagKey] 275 } 276 277 var apiServiceImage, controllerImage, defaultServiceType string 278 var controllerLabels, apiServiceLabels, apiServiceAnnotations map[string]string 279 280 for _, deploy := range apps.Items { 281 if deploy.Labels == nil { 282 continue 283 } 284 switch deploy.Labels[fryKey] { 285 case composeFry: 286 controllerImage = deploy.Spec.Template.Spec.Containers[0].Image 287 controllerLabels = deploy.Labels 288 case composeAPIServerFry: 289 apiServiceImage = deploy.Spec.Template.Spec.Containers[0].Image 290 apiServiceLabels = deploy.Labels 291 apiServiceAnnotations = deploy.Annotations 292 } 293 if svcType, ok := deploy.Labels[defaultServiceTypeKey]; ok { 294 defaultServiceType = svcType 295 } 296 } 297 298 return Status{ 299 IsInstalled: true, 300 Tag: tag, 301 IsCrdPresent: isCrdPresent, 302 Namespace: apps.Items[0].Namespace, 303 ControllerImage: controllerImage, 304 APIServiceImage: apiServiceImage, 305 DefaultServiceType: defaultServiceType, 306 ControllerLabels: controllerLabels, 307 APIServiceLabels: apiServiceLabels, 308 APIServiceAnnotations: apiServiceAnnotations, 309 }, nil 310 } 311 312 func (s Status) match(c *installer) (bool, string) { 313 // make sure debug tags are never a match 314 if c.commonOptions.Tag == "debug" || s.Tag == "debug" { 315 return false, "force redeploy if desired or current state is in debug mode" 316 } 317 if s.Tag == c.commonOptions.Tag && s.DefaultServiceType == c.commonOptions.DefaultServiceType { 318 // check customTLSHash 319 320 if s.APIServiceAnnotations == nil && c.customTLSHash != "" { 321 return false, "Custom TLS hash mismatch" 322 } 323 324 if s.APIServiceAnnotations != nil { 325 actualValue := s.APIServiceAnnotations[customTLSHashAnnotationName] 326 if actualValue != c.customTLSHash { 327 return false, "Custom TLS hash mismatch" 328 } 329 } 330 331 // check custom matches 332 if c.customMatch == nil || c.customMatch(s) { 333 return true, fmt.Sprintf("Compose version %s is already installed in namespace %q with the same settings", c.commonOptions.Tag, s.Namespace) 334 } 335 } 336 return false, fmt.Sprintf("An older version is installed in namespace %q. Uninstalling...", s.Namespace) 337 } 338 339 func (c *installer) install(ctx context.Context) error { 340 log.Info("Checking installation state") 341 installStatus, err := c.isInstalled() 342 if err != nil { 343 return err 344 } 345 if err := c.validateOptions(); err != nil { 346 return err 347 } 348 if installStatus.IsInstalled && !c.controllerOnly { 349 match, message := installStatus.match(c) 350 log.Info(message) 351 if match { 352 return nil 353 } 354 355 if err = Uninstall(c.config, installStatus.Namespace, false); err != nil { 356 return err 357 } 358 if err = WaitForUninstallCompletion(ctx, c.config, installStatus.Namespace, false); err != nil { 359 return err 360 } 361 } 362 log.Infof("Install image with tag %q in namespace %q", c.commonOptions.Tag, c.commonOptions.Namespace) 363 ictx := &installerContext{} 364 var steps []func(*installerContext) error 365 if c.controllerOnly { 366 steps = []func(*installerContext) error{ 367 c.createNamespace, 368 c.createPullSecretIfRequired, 369 c.createServiceAccount, 370 c.createClusterRoleBindings, 371 c.createController, 372 } 373 } else { 374 steps = []func(*installerContext) error{ 375 c.createNamespace, 376 c.createPullSecretIfRequired, 377 c.createServiceAccount, 378 c.createClusterRoleBindings, 379 c.createEtcdSecret, 380 c.createNetworkSecret, 381 c.createAPIServer, 382 } 383 if !c.disableController { 384 steps = append(steps, c.createController) 385 } 386 } 387 steps = append(steps, c.createDefaultClusterRoles) 388 for _, step := range steps { 389 if err := step(ictx); err != nil { 390 return err 391 } 392 } 393 return nil 394 }