content: // Licensed to the Apache Software Foundation (ASF) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The ASF licenses this file
  // to you under the Apache License, Version 2.0 (the
  // "License"); you may not use this file except in compliance
  // with the License.  You may obtain a copy of the License at
  //
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing,
  // software distributed under the License is distributed on an
  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  // KIND, either express or implied.  See the License for the
  // specific language governing permissions and limitations
  // under the License.
  
  package com.cloud.kubernetes.version;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import javax.inject.Inject;
  
  import org.apache.cloudstack.api.ApiCommandResourceType;
  import org.apache.cloudstack.api.ApiConstants;
  import org.apache.cloudstack.api.command.admin.kubernetes.version.AddKubernetesSupportedVersionCmd;
  import org.apache.cloudstack.api.command.admin.kubernetes.version.DeleteKubernetesSupportedVersionCmd;
  import org.apache.cloudstack.api.command.admin.kubernetes.version.UpdateKubernetesSupportedVersionCmd;
  import org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd;
  import org.apache.cloudstack.api.command.user.iso.RegisterIsoCmd;
  import org.apache.cloudstack.api.command.user.kubernetes.version.ListKubernetesSupportedVersionsCmd;
  import org.apache.cloudstack.api.response.KubernetesSupportedVersionResponse;
  import org.apache.cloudstack.api.response.ListResponse;
  import org.apache.cloudstack.context.CallContext;
  import org.apache.commons.lang3.StringUtils;
  
  import com.cloud.api.query.dao.TemplateJoinDao;
  import com.cloud.api.query.vo.TemplateJoinVO;
  import com.cloud.cpu.CPU;
  import com.cloud.dc.DataCenter;
  import com.cloud.dc.DataCenterVO;
  import com.cloud.dc.dao.DataCenterDao;
  import com.cloud.event.ActionEvent;
  import com.cloud.exception.InvalidParameterValueException;
  import com.cloud.exception.ResourceAllocationException;
  import com.cloud.kubernetes.cluster.KubernetesClusterService;
  import com.cloud.kubernetes.cluster.KubernetesClusterVO;
  import com.cloud.kubernetes.cluster.dao.KubernetesClusterDao;
  import com.cloud.kubernetes.version.dao.KubernetesSupportedVersionDao;
  import com.cloud.storage.VMTemplateVO;
  import com.cloud.storage.dao.VMTemplateDao;
  import com.cloud.storage.dao.VMTemplateZoneDao;
  import com.cloud.template.TemplateApiService;
  import com.cloud.template.VirtualMachineTemplate;
  import com.cloud.user.AccountManager;
  import com.cloud.utils.Pair;
  import com.cloud.utils.component.ComponentContext;
  import com.cloud.utils.component.ManagerBase;
  import com.cloud.utils.db.Filter;
  import com.cloud.utils.db.JoinBuilder;
  import com.cloud.utils.db.SearchBuilder;
  import com.cloud.utils.db.SearchCriteria;
  import com.cloud.utils.exception.CloudRuntimeException;
  
  public class KubernetesVersionManagerImpl extends ManagerBase implements KubernetesVersionService {
  
      @Inject
      private KubernetesSupportedVersionDao kubernetesSupportedVersionDao;
      @Inject
      private KubernetesClusterDao kubernetesClusterDao;
      @Inject
      private AccountManager accountManager;
      @Inject
      private VMTemplateDao templateDao;
      @Inject
      private TemplateJoinDao templateJoinDao;
      @Inject
      private VMTemplateZoneDao templateZoneDao;
      @Inject
      private DataCenterDao dataCenterDao;
      @Inject
      private TemplateApiService templateService;
  
      public static final String MINIMUN_AUTOSCALER_SUPPORTED_VERSION = "1.15.0";
  
      protected void updateTemplateDetailsInKubernetesSupportedVersionResponse(
              final KubernetesSupportedVersion kubernetesSupportedVersion, KubernetesSupportedVersionResponse response) {
          TemplateJoinVO template = templateJoinDao.findById(kubernetesSupportedVersion.getIsoId());
          if (template == null) {
              return;
          }
          response.setIsoId(template.getUuid());
          response.setIsoName(template.getName());
          if (template.getState() != null) {
              response.setIsoState(template.getState().toString());
          }
          response.setIsoArch(template.getArch().getType());
          response.setDirectDownload(template.isDirectDownload());
      }
  
      private KubernetesSupportedVersionResponse createKubernetesSupportedVersionResponse(final KubernetesSupportedVersion kubernetesSupportedVersion) {
          KubernetesSupportedVersionResponse response = new KubernetesSupportedVersionResponse();
          response.setObjectName("kubernetessupportedversion");
          response.setId(kubernetesSupportedVersion.getUuid());
          response.setName(kubernetesSupportedVersion.getName());
          response.setSemanticVersion(kubernetesSupportedVersion.getSemanticVersion());
          if (kubernetesSupportedVersion.getState() != null) {
              response.setState(kubernetesSupportedVersion.getState().toString());
          }
          response.setMinimumCpu(kubernetesSupportedVersion.getMinimumCpu());
          response.setMinimumRamSize(kubernetesSupportedVersion.getMinimumRamSize());
          DataCenterVO zone = dataCenterDao.findById(kubernetesSupportedVersion.getZoneId());
          if (zone != null) {
              response.setZoneId(zone.getUuid());
              response.setZoneName(zone.getName());
          }
          response.setSupportsHA(compareSemanticVersions(kubernetesSupportedVersion.getSemanticVersion(),
              KubernetesClusterService.MIN_KUBERNETES_VERSION_HA_SUPPORT)>=0);
          response.setSupportsAutoscaling(versionSupportsAutoscaling(kubernetesSupportedVersion));
          updateTemplateDetailsInKubernetesSupportedVersionResponse(kubernetesSupportedVersion, response);
          response.setCreated(kubernetesSupportedVersion.getCreated());
          return response;
      }
  
      private ListResponse<KubernetesSupportedVersionResponse> createKubernetesSupportedVersionListResponse(
              List<KubernetesSupportedVersionVO> versions, Integer count) {
          List<KubernetesSupportedVersionResponse> responseList = new ArrayList<>();
          for (KubernetesSupportedVersionVO version : versions) {
              responseList.add(createKubernetesSupportedVersionResponse(version));
          }
          ListResponse<KubernetesSupportedVersionResponse> response = new ListResponse<>();
          response.setResponses(responseList, count);
          return response;
      }
  
      private static boolean isSemanticVersion(final String version) {
          if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
              return false;
          }
          String[] parts = version.split("\\.");
          if (parts.length < 3) {
              return false;
          }
          return true;
      }
  
      private List <KubernetesSupportedVersionVO> filterKubernetesSupportedVersions(List <KubernetesSupportedVersionVO> versions, final String minimumSemanticVersion) {
          if (StringUtils.isNotEmpty(minimumSemanticVersion)) {
              for (int i = versions.size() - 1; i >= 0; --i) {
                  KubernetesSupportedVersionVO version = versions.get(i);
                  try {
                      if (compareSemanticVersions(minimumSemanticVersion, version.getSemanticVersion()) > 0) {
                          versions.remove(i);
                      }
                  } catch (IllegalArgumentException e) {
                      logger.warn("Unable to compare Kubernetes version for supported version {} with {}", version, minimumSemanticVersion);
                      versions.remove(i);
                  }
              }
          }
          return versions;
      }
  
      private VirtualMachineTemplate registerKubernetesVersionIso(final Long zoneId, final String versionName, final String isoUrl, final String isoChecksum, final boolean directDownload, CPU.CPUArch arch) throws IllegalAccessException, NoSuchFieldException,
              IllegalArgumentException, ResourceAllocationException {
          CallContext.register(CallContext.current(), ApiCommandResourceType.Iso);
          String isoName = String.format("%s-Kubernetes-Binaries-ISO", versionName);
          RegisterIsoCmd registerIsoCmd = new RegisterIsoCmd();
          registerIsoCmd = ComponentContext.inject(registerIsoCmd);
          registerIsoCmd.setIsoName(isoName);
          registerIsoCmd.setPublic(true);
          if (zoneId != null) {
              registerIsoCmd.setZoneId(zoneId);
          }
          registerIsoCmd.setDisplayText(isoName);
          registerIsoCmd.setBootable(false);
          registerIsoCmd.setUrl(isoUrl);
          if (StringUtils.isNotEmpty(isoChecksum)) {
              registerIsoCmd.setChecksum(isoChecksum);
          }
          registerIsoCmd.setDirectDownload(directDownload);
          registerIsoCmd.setArch(arch.getType());
          registerIsoCmd.setAccountName(accountManager.getSystemAccount().getAccountName());
          registerIsoCmd.setDomainId(accountManager.getSystemAccount().getDomainId());
          try {
              return templateService.registerIso(registerIsoCmd);
          } finally {
              CallContext.unregister();
          }
      }
  
      private void deleteKubernetesVersionIso(long templateId) throws IllegalAccessException, NoSuchFieldException,
              IllegalArgumentException {
          CallContext isoContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Iso);
          isoContext.setEventResourceId(templateId);
          DeleteIsoCmd deleteIsoCmd = new DeleteIsoCmd();
          deleteIsoCmd = ComponentContext.inject(deleteIsoCmd);
          deleteIsoCmd.setId(templateId);
          try {
              templateService.deleteIso(deleteIsoCmd);
          } finally {
              CallContext.unregister();
          }
      }
  
      public static int compareSemanticVersions(String v1, String v2) throws IllegalArgumentException {
          if (StringUtils.isAnyEmpty(v1, v2)) {
              throw new IllegalArgumentException(String.format("Invalid version comparision with versions %s, %s", v1, v2));
          }
          if(!isSemanticVersion(v1)) {
              throw new IllegalArgumentException(String.format("Invalid version format, %s. Semantic version should be specified in MAJOR.MINOR.PATCH format", v1));
          }
          if(!isSemanticVersion(v2)) {
              throw new IllegalArgumentException(String.format("Invalid version format, %s. Semantic version should be specified in MAJOR.MINOR.PATCH format", v2));
          }
          String[] thisParts = v1.split("\\.");
          String[] thatParts = v2.split("\\.");
          int length = Math.max(thisParts.length, thatParts.length);
          for(int i = 0; i < length; i++) {
              int thisPart = i < thisParts.length ?
                      Integer.parseInt(thisParts[i]) : 0;
              int thatPart = i < thatParts.length ?
                      Integer.parseInt(thatParts[i]) : 0;
              if(thisPart < thatPart)
                  return -1;
              if(thisPart > thatPart)
                  return 1;
          }
          return 0;
      }
  
      public static boolean versionSupportsAutoscaling(KubernetesSupportedVersion clusterVersion) {
          return clusterVersion.getSemanticVersion().compareTo(MINIMUN_AUTOSCALER_SUPPORTED_VERSION) >= 0;
      }
  
      /**
       * Returns a boolean value whether Kubernetes cluster upgrade can be carried from a given currentVersion to upgradeVersion
       * Kubernetes clusters can only be upgraded from one MINOR version to the next MINOR version, or between PATCH versions of the same MINOR.
       * That is, MINOR versions cannot be skipped during upgrade.
       * For example, you can upgrade from 1.y to 1.y+1, but not from 1.y to 1.y+2
       * @param currentVersion
       * @param upgradeVersion
       * @return
       * @throws IllegalArgumentException
       */
      public static boolean canUpgradeKubernetesVersion(final String currentVersion, final String upgradeVersion) throws IllegalArgumentException {
          int versionDiff = compareSemanticVersions(upgradeVersion, currentVersion);
          if (versionDiff < 0) {
              throw new IllegalArgumentException(String.format("Kubernetes clusters can not be downgraded, current version: %s, upgrade version: %s", currentVersion, upgradeVersion));
          }
          String[] thisParts = currentVersion.split("\\.");
          String[] thatParts = upgradeVersion.split("\\.");
          int majorVerDiff = Integer.parseInt(thatParts[0]) - Integer.parseInt(thisParts[0]);
          int minorVerDiff = Integer.parseInt(thatParts[1]) - Integer.parseInt(thisParts[1]);
  
          if (majorVerDiff != 0 || minorVerDiff > 1) {
              throw new IllegalArgumentException(String.format("Kubernetes clusters can be upgraded between next minor or patch version releases, current version: %s, upgrade version: %s", currentVersion, upgradeVersion));
          }
          return true;
      }
  
      @Override
      public ListResponse<KubernetesSupportedVersionResponse> listKubernetesSupportedVersions(final ListKubernetesSupportedVersionsCmd cmd) {
          if (!KubernetesClusterService.KubernetesServiceEnabled.value()) {
              throw new CloudRuntimeException("Kubernetes Service plugin is disabled");
          }
          final Long versionId = cmd.getId();
          final Long zoneId = cmd.getZoneId();
          String minimumSemanticVersion = cmd.getMinimumSemanticVersion();
          final Long minimumKubernetesVersionId = cmd.getMinimumKubernetesVersionId();
          final String arch = cmd.getArch();
          if (StringUtils.isNotEmpty(minimumSemanticVersion) && minimumKubernetesVersionId != null) {
              throw new CloudRuntimeException(String.format("Both parameters %s and %s can not be passed together", ApiConstants.MIN_SEMANTIC_VERSION, ApiConstants.MIN_KUBERNETES_VERSION_ID));
          }
          if (minimumKubernetesVersionId != null) {
              KubernetesSupportedVersionVO minVersion = kubernetesSupportedVersionDao.findById(minimumKubernetesVersionId);
              if (minVersion == null) {
                  throw new InvalidParameterValueException(String.format("Invalid %s passed", ApiConstants.MIN_KUBERNETES_VERSION_ID));
              }
              minimumSemanticVersion = minVersion.getSemanticVersion();
          }
          Filter searchFilter = new Filter(KubernetesSupportedVersionVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal());
          SearchBuilder<KubernetesSupportedVersionVO> sb = kubernetesSupportedVersionDao.createSearchBuilder();
          sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
          sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);
          if (StringUtils.isNotBlank(arch)) {
              SearchBuilder<VMTemplateVO> isoSearch = templateDao.createSearchBuilder();
              isoSearch.and("arch", isoSearch.entity().getArch(), SearchCriteria.Op.EQ);
              sb.join("isoSearch", isoSearch, isoSearch.entity().getId(), sb.entity().getIsoId(), JoinBuilder.JoinType.INNER);
              isoSearch.done();
          }
          sb.done();
          SearchCriteria<KubernetesSupportedVersionVO> sc = sb.create();
          String keyword = cmd.getKeyword();
          if (versionId != null) {
              sc.setParameters("id", versionId);
          }
          if (zoneId != null) {
              SearchCriteria<KubernetesSupportedVersionVO> scc = kubernetesSupportedVersionDao.createSearchCriteria();
              scc.addOr("zoneId", SearchCriteria.Op.EQ, zoneId);
              scc.addOr("zoneId", SearchCriteria.Op.NULL);
              sc.addAnd("zoneId", SearchCriteria.Op.SC, scc);
          }
          if(keyword != null){
              sc.setParameters("keyword", "%" + keyword + "%");
          }
          if (StringUtils.isNotBlank(arch)) {
              sc.setJoinParameters("isoSearch", "arch", arch);
          }
          Pair<List<KubernetesSupportedVersionVO>, Integer> versionsAndCount =
                  kubernetesSupportedVersionDao.searchAndCount(sc, searchFilter);
          List<KubernetesSupportedVersionVO> versions =
                  filterKubernetesSupportedVersions(versionsAndCount.first(), minimumSemanticVersion);
  
          return createKubernetesSupportedVersionListResponse(versions, versionsAndCount.second());
      }
  
      @Override
      @ActionEvent(eventType = KubernetesVersionEventTypes.EVENT_KUBERNETES_VERSION_ADD,
              eventDescription = "Adding Kubernetes supported version")
      public KubernetesSupportedVersionResponse addKubernetesSupportedVersion(final AddKubernetesSupportedVersionCmd cmd) {
          if (!KubernetesClusterService.KubernetesServiceEnabled.value()) {
              throw new CloudRuntimeException("Kubernetes Service plugin is disabled");
          }
          String name = cmd.getName();
          final String semanticVersion = cmd.getSemanticVersion();
          final Long zoneId = cmd.getZoneId();
          final String isoUrl = cmd.getUrl();
          final String isoChecksum = cmd.getChecksum();
          final Integer minimumCpu = cmd.getMinimumCpu();
          final Integer minimumRamSize = cmd.getMinimumRamSize();
          final boolean isDirectDownload = cmd.isDirectDownload();
          CPU.CPUArch arch = cmd.getArch();
  
          if (minimumCpu == null || minimumCpu < KubernetesClusterService.MIN_KUBERNETES_CLUSTER_NODE_CPU) {
              throw new InvalidParameterValueException(String.format("Invalid value for %s parameter. Minimum %d vCPUs required.", ApiConstants.MIN_CPU_NUMBER, KubernetesClusterService.MIN_KUBERNETES_CLUSTER_NODE_CPU));
          }
          if (minimumRamSize == null || minimumRamSize < KubernetesClusterService.MIN_KUBERNETES_CLUSTER_NODE_RAM_SIZE) {
              throw new InvalidParameterValueException(String.format("Invalid value for %s parameter. Minimum %dMB memory required", ApiConstants.MIN_MEMORY, KubernetesClusterService.MIN_KUBERNETES_CLUSTER_NODE_RAM_SIZE));
          }
          if (compareSemanticVersions(semanticVersion, MIN_KUBERNETES_VERSION) < 0) {
              throw new InvalidParameterValueException(String.format("New supported Kubernetes version cannot be added as %s is minimum version supported by Kubernetes Service", MIN_KUBERNETES_VERSION));
          }
          if (zoneId != null) {
              DataCenter zone = dataCenterDao.findById(zoneId);
              if (zone == null) {
                  throw new InvalidParameterValueException("Invalid zone specified");
              }
              if (DataCenter.Type.Edge.equals(zone.getType()) && !isDirectDownload) {
                  throw new InvalidParameterValueException(String.format("Zone: %s supports only direct download Kubernetes versions", zone.getName()));
              }
          }
          if (StringUtils.isEmpty(isoUrl)) {
              throw new InvalidParameterValueException(String.format("Invalid URL for ISO specified, %s", isoUrl));
          }
          if (StringUtils.isEmpty(name)) {
              name = String.format("v%s", semanticVersion);
              if (zoneId != null) {
                  name = String.format("%s-%s", name, dataCenterDao.findById(zoneId).getName());
              }
          }
  
          VMTemplateVO template = null;
          try {
              VirtualMachineTemplate vmTemplate = registerKubernetesVersionIso(zoneId, name, isoUrl, isoChecksum, isDirectDownload, arch);
              template = templateDao.findById(vmTemplate.getId());
          } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException | ResourceAllocationException ex) {
              logger.error(String.format("Unable to register binaries ISO for supported kubernetes version, %s, with url: %s", name, isoUrl), ex);
              throw new CloudRuntimeException(String.format("Unable to register binaries ISO for supported kubernetes version, %s, with url: %s", name, isoUrl));
          }
  
          KubernetesSupportedVersionVO supportedVersionVO = new KubernetesSupportedVersionVO(name, semanticVersion, template.getId(), zoneId, minimumCpu, minimumRamSize);
          supportedVersionVO = kubernetesSupportedVersionDao.persist(supportedVersionVO);
          CallContext.current().putContextParameter(KubernetesSupportedVersion.class, supportedVersionVO.getUuid());
  
          return createKubernetesSupportedVersionResponse(supportedVersionVO);
      }
  
      @Override
      @ActionEvent(eventType = KubernetesVersionEventTypes.EVENT_KUBERNETES_VERSION_DELETE,
              eventDescription = "deleting Kubernetes supported version", async = true)
      public boolean deleteKubernetesSupportedVersion(final DeleteKubernetesSupportedVersionCmd cmd) {
          if (!KubernetesClusterService.KubernetesServiceEnabled.value()) {
              throw new CloudRuntimeException("Kubernetes Service plugin is disabled");
          }
          final Long versionId = cmd.getId();
          KubernetesSupportedVersion version = kubernetesSupportedVersionDao.findById(versionId);
          if (version == null) {
              throw new InvalidParameterValueException("Invalid Kubernetes version id specified");
          }
          List<KubernetesClusterVO> clusters = kubernetesClusterDao.listAllByKubernetesVersion(versionId);
          if (clusters.size() > 0) {
              throw new CloudRuntimeException(String.format("Unable to delete Kubernetes version ID: %s. Existing clusters currently using the version.", version.getUuid()));
          }
  
          VMTemplateVO template = templateDao.findByIdIncludingRemoved(version.getIsoId());
          if (template == null) {
              logger.warn("Unable to find ISO associated with supported Kubernetes version {}", version);
          }
          if (template != null && template.getRemoved() == null) { // Delete ISO
              try {
                  deleteKubernetesVersionIso(template.getId());
              } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException ex) {
                  logger.error("Unable to delete binaries ISO: {} associated with supported kubernetes version: {}", template, version, ex);
                  throw new CloudRuntimeException(String.format("Unable to delete binaries ISO ID: %s associated with supported kubernetes version ID: %s", template.getUuid(), version.getUuid()));
              }
          }
          return kubernetesSupportedVersionDao.remove(version.getId());
      }
  
      @Override
      @ActionEvent(eventType = KubernetesVersionEventTypes.EVENT_KUBERNETES_VERSION_UPDATE,
              eventDescription = "Updating Kubernetes supported version")
      public KubernetesSupportedVersionResponse updateKubernetesSupportedVersion(final UpdateKubernetesSupportedVersionCmd cmd) {
          if (!KubernetesClusterService.KubernetesServiceEnabled.value()) {
              throw new CloudRuntimeException("Kubernetes Service plugin is disabled");
          }
          final Long versionId = cmd.getId();
          KubernetesSupportedVersion.State state = null;
          KubernetesSupportedVersionVO version = kubernetesSupportedVersionDao.findById(versionId);
          if (version == null) {
              throw new InvalidParameterValueException("Invalid Kubernetes version id specified");
          }
          try {
              state = KubernetesSupportedVersion.State.valueOf(cmd.getState());
          } catch (IllegalArgumentException iae) {
              throw new InvalidParameterValueException(String.format("Invalid value for %s parameter", ApiConstants.STATE));
          }
          if (!state.equals(version.getState())) {
              version = kubernetesSupportedVersionDao.createForUpdate(version.getId());
              version.setState(state);
              if (!kubernetesSupportedVersionDao.update(version.getId(), version)) {
                  throw new CloudRuntimeException(String.format("Failed to update Kubernetes supported version ID: %s", version.getUuid()));
              }
              version = kubernetesSupportedVersionDao.findById(versionId);
          }
          return  createKubernetesSupportedVersionResponse(version);
      }
  
      @Override
      public List<Class<?>> getCommands() {
          List<Class<?>> cmdList = new ArrayList<Class<?>>();
          if (!KubernetesClusterService.KubernetesServiceEnabled.value()) {
              return cmdList;
          }
          cmdList.add(AddKubernetesSupportedVersionCmd.class);
          cmdList.add(ListKubernetesSupportedVersionsCmd.class);
          cmdList.add(DeleteKubernetesSupportedVersionCmd.class);
          cmdList.add(UpdateKubernetesSupportedVersionCmd.class);
          return cmdList;
      }
  }
  