Spinnaker第八節—Deck代碼詳解

前言

  說實話作爲一個後端語言的工程師來說,Spinnaker的Deck代碼是我研究起來最費勁的,因爲Spinnaker的UI設計並沒有採用antd之類的前端腳手架,而是純靠angular+typescript+html一點點寫出來的,不僅需要有這些js語言的基礎,還要找到項目內代碼之間的引用關係,對於js項目來說引用關係確實需要花時間一點點的去review代碼。不過經過努力最終還是啃下了,越是困難的事情,越是有收穫。

Review入口

  Spinnaker所有跟雲廠商有關的微服務節點的目錄結構都是核心的放在core內,每家雲私有的部分放在以自己縮寫的目錄中,deck也遵循這個規則。以阿里云爲例,review入口從alicloud.module.tx中開始。

  CloudProviderRegistry.registerProvider('alicloud', {
    name: 'AlibabaCloud',
    logo: {
      path: require('./logo/alicloud.png'),
    },
    image: {
      reader: 'alicloudImageReader',
    },
    serverGroup: {
      transformer: 'alicloudServerGroupTransformer',
      detailsTemplateUrl: require('./serverGroup/details/serverGroupDetails.html'),
      detailsController: 'alicloudServerGroupDetailsCtrl',
      // CloneServerGroupModal: CloneServerGroupAlicloud,
      cloneServerGroupTemplateUrl: require('./serverGroup/configure/wizard/serverGroupWizard.html'),
      cloneServerGroupController: 'alicloudCloneServerGroupCtrl',
      commandBuilder: 'alicloudServerGroupCommandBuilder',
      configurationService: 'alicloudServerGroupConfigurationService',
    },
    instance: {
      instanceTypeService: 'alicloudInstanceTypeService',
      detailsTemplateUrl: require('./instance/details/instanceDetails.html'),
      detailsController: 'alicloudInstanceDetailsCtrl',
    },
    securityGroup: {
      transformer: 'alicloudSecurityGroupTransformer',
      writer: 'alicloudSecurityGroupWriter',
      reader: 'alicloudSecurityGroupReader',
      detailsTemplateUrl: require('./securityGroup/details/securityGroupDetail.html'),
      detailsController: 'alicloudSecurityGroupDetailsCtrl',
      // CreateSecurityGroupModal: CreateSecurityGroup,
      createSecurityGroupTemplateUrl: require('./securityGroup/configure/createSecurityGroup.html'),
      createSecurityGroupController: 'alicloudCreateSecurityGroupCtrl',
    },
    loadBalancer: {
      transformer: 'alicloudLoadBalancerTransformer',
      detailsTemplateUrl: require('./loadBalancer/details/loadBalancerDetail.html'),
      detailsController: 'alicloudLoadBalancerDetailsCtrl',
      // CreateLoadBalancerModal: ConfigureLoadBalancerModal,
      createLoadBalancerTemplateUrl: require('./loadBalancer/configure/createLoadBalancer.html'),
      createLoadBalancerController: 'alicloudCreateLoadBalancerCtrl',
    },
  });

此處註冊了阿里雲的服務,定義了名稱與圖標, 圖標的展示以及前端UI據大部分的展示已經在core中實現了,而這裏定義的都是自定義的部分。

image定義的是鏡像相關的內容,因爲在deploy和彈性伸縮的創建時需要與雲平臺交互取到鏡像列表的,這部分雲廠商接口都不一樣,所以要個性處理。爲什麼沒有writer呢?因爲Spinnaker的鏡像製作是又bake階段實現的,交給了rosco這個微服務節點,而rosco本質也是將http請求轉化爲packer命令,所以鏡像的製作是由packer來做的,spinnaker代碼本身並未涉及。

servergroup是spinnaker的核心對象,是專門處理彈性伸縮的,當然它並不是孤立的,無論哪家雲平臺,彈性伸縮部分也都需要與鏡像、實例類型、安全組、LoadBalance這些關聯的,所以servergroup並不能孤立的使用。

transformer代碼在serverGroup.transformer.ts,convertServerGroupCommandToDeployConfiguration方法將command轉化爲configuration,最終會被serverGroupWirter.ts調用。

detailsTemplateUrl和detailsController這是一對大寶貝,前者是右側詳細信息的html模板,後者是該模板的數據來源服務。功能位置:

核心代碼在serverGroupDetails.alicloud.controller.ts的retrieveServerGroup()方法,可見數據是從ServerGroupReader中來的:

function retrieveServerGroup() {
        const summary = extractServerGroupSummary();
        return ServerGroupReader.getServerGroup(
          app.name,
          serverGroup.accountId,
          serverGroup.region,
          serverGroup.name,
        ).then(function(details: any) {
          cancelLoader();
          angular.extend(details, summary);
          details.account = serverGroup.accountId;
          $scope.serverGroup = details;
          if (!_.isEmpty($scope.serverGroup)) {
            $scope.image = details.image ? details.image : undefined;
            if (details.image && details.image.description) {
              const tags: any[] = details.image.description.split(',');
              tags.forEach(function(tag) {
                const keyVal: any[] = tag.split('=');
                if (keyVal.length === 2 && keyVal[0] === 'ancestor_name') {
                  details.image.baseImage = keyVal[1];
                }
              });
            }
            if (details.launchConfig && details.launchConfig.securityGroups) {
              $scope.securityGroups = _.chain(details.launchConfig.securityGroups)
                .map(function(id: any) {
                  return (
                    _.find(app.securityGroups.data, {
                      accountName: serverGroup.accountId,
                      region: serverGroup.region,
                      id: id,
                    }) ||
                    _.find(app.securityGroups.data, {
                      accountName: serverGroup.accountId,
                      region: serverGroup.region,
                      name: id,
                    })
                  );
                })
                .compact()
                .value();
            }
          } else {
            $state.go('^');
          }
        });
      }

 

commandBuilder,js中的代理模式,此處不好理解,需要詳細說明,以buildServerGroupCommandFromPipeline爲例。
      modules\alicloud\src\serverGroup\configure\serverGroupCommandBuilder.service.ts中定義了buildServerGroupCommandFromPipeline的實現,構造一個command複雜對象。       
      modules\core\src\serverGroup\configure\common\serverGroupCommandBuilder.service.ts中通過代理模式對其進行引用
      modules\core\src\pipeline\config\stages\deploy\deployStage.js是真正的調用者

configurationService 實例類型、lb列表框、安全組列表框等配套服務,正如前文所說,彈性伸縮組與這些配套資源需要一起使用。

把ServerGroup的註冊部分看完,剩下的也就迎刃而解了instance、securityGroup、loadBalancer,都是類似的規則。

Pipeline原理

Spinnaker中對資源的增刪改查分爲兩種方式,第一種是INFRASTRUCTURE中對彈性伸縮、LoadBalance、安全組這三種資源的直接操作;第二種是將其封裝成一個個的固化的stage集成到pipeline流水線中。我們這裏看下pipeline的前端代碼是怎麼實現的。Pipeline的核心是Registry.pipeline.registerStage:註冊Stage。

以tagImageStage爲例代碼在modules\alicloud\src\pipeline\stages\tagImage\alicloudTagImageStage.js

    Registry.pipeline.registerStage({
      provides: 'upsertImageTags',
      cloudProvider: 'alicloud',
      templateUrl: require('./tagImageStage.html'),
      executionDetailsUrl: require('./tagImageExecutionDetails.html'),
      executionConfigSections: ['tagImageConfig', 'taskStatus'],
      validators: [
        { type: 'requiredField', fieldName: 'region' },
        { type: 'requiredField', fieldName: 'tags' },
      ],
    });

provides: stage的標識;cloudProvider: 雲平臺標識;templateUrl: stage的配置頁面;executionDetailsUrl:pipeline中stage詳情展示;executionConfigSections: pipeline執行信息中的導航;validators: form校驗。通過這些關聯關係的配置,將前面說講的對資源的操作封裝到了stage中來。

再來看這個環節註冊器Registry.ts的代碼,分析registerStage的細節,看到就2行代碼:

  public registerStage(stageConfig: IStageTypeConfig): void {
    this.stageTypes.push(stageConfig);
    this.normalizeStageTypes();
  }

 stageTypes是IStageTypeConfig數組(IStageTypeConfig.ts中定義),normalizeStageTypes中有一個對parent的處理,具有parent的如果有些默認值爲空,可以從同provides的parent中繼承。

現在知道如何存的了,下面看如何查找的。pipelineConfigurer.html是pipeline配置頁面,聚焦pipelineConfigurerCtrl.addStage()方法,但這裏看到的只是流程圖的變更,並沒有定位到具體的stage頁面,一直找下去,其實在這裏stage.module.js

  .directive('pipelineConfigStage', function() {
    return {
      restrict: 'E',
      require: '^pipelineConfigurer',
      scope: {
        viewState: '=',
        application: '=',
        pipeline: '=',
        stageFieldUpdated: '<',
      },
      controller: 'StageConfigCtrl as stageConfigCtrl',
      templateUrl: require('./stage.html'),
      link: function(scope, elem, attrs, pipelineConfigurerCtrl) {
        scope.pipelineConfigurerCtrl = pipelineConfigurerCtrl;
      },
    };
  })

跟入controller中,發現取的是options.stageTypes,核心代碼:$scope.options.stageTypes = Registry.pipeline.getConfigurableStageTypes(accounts);回到Registry.pipeline中找到相關代碼: 

  public getConfigurableStageTypes(accounts?: IAccountDetails[]): IStageTypeConfig[] {
    const providers: string[] = isNil(accounts) ? [] : Array.from(new Set(accounts.map(a => a.cloudProvider)));
    const allStageTypes = this.getStageTypes();
    let configurableStageTypes = allStageTypes.filter(stageType => !stageType.synthetic && !stageType.provides);
    if (providers.length === 0) {
      return configurableStageTypes;
    }
    configurableStageTypes.forEach(
      type => (type.cloudProviders = this.getCloudProvidersForStage(type, allStageTypes, accounts)),
    );
    configurableStageTypes = configurableStageTypes.filter(type => {
      return !accounts.every(a => {
        const p = CloudProviderRegistry.getProvider(a.cloudProvider, a.skin);
        return isExcludedStageType(type, p);
      });
    });
    return configurableStageTypes
      .filter(stageType => stageType.cloudProviders.length)
      .sort((a, b) => a.label.localeCompare(b.label));
  }

Reader和Writer原理

以鏡像讀取reader爲例modules\core\src\image\image.reader.ts,看得出使用的是代理模式:

  public findImages(params: IFindImageParams): IPromise<IImage[]> {
    return this.getDelegate(params.provider).findImages(params);
  }
  
  private getDelegate(cloudProvider: string): IImageReader {
    return this.providerServiceDelegate.getDelegate<IImageReader>(cloudProvider, 'image.reader');
  }

服務的註冊和獲取在core/cloudProvider/providerService.delegate中:

  public getDelegate<T>(provider: string, serviceKey: string, skin?: string): T {
    const service = CloudProviderRegistry.getValue(provider, serviceKey, skin);
    if (isString(service) && this.$injector.has(service)) {
      // service is a string, it's an AngularJS service
      return this.$injector.get<T>(service, 'providerDelegate');
    } else if (isFunction(service)) {
      // service is a Function, assume it's service class, so new() it
      return new service();
    } else {
      throw new Error('No "' + serviceKey + '" service found for provider "' + provider + '"');
    }
  }

  可見providerService這一層也只是邏輯層,真正存儲維護都交給了CloudProviderRegistry,其中有getValue方法,也必有setValue方法,而setValue方法的調用的內容就在剛開始介紹的alicloud.module.tx中。所以core中image.reader通過alicloud.module.tx的代理註冊,最終調用的是阿里雲自己項目中的image.reader。

瞭解了reader,writer以及其他service本質上都一樣了,都是core中通過代理調用了雲廠商私有的實現。 

總結

  1 ${clouddriver}.module.tx是最重要的入口,裏面的內容必須要掌握。
  2 pipeline中代碼是對資源stage化的封裝,真正與後端的交互邏輯還是在資源本身的目錄中尋找。
  3 core中通過代理模式獲取到雲平臺私有實現。
  4 留意下modules\core\src\forms下的自定義組件,自己做功能擴展時優先用現成的,保持統一風格。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章