1   /*
2    * Copyright 2023 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   *
16   */
17  
18  package com.linecorp.centraldogma.server.internal.storage.repository;
19  
20  import static com.google.common.base.MoreObjects.firstNonNull;
21  import static java.util.Objects.requireNonNull;
22  
23  import java.net.URI;
24  import java.util.List;
25  import java.util.Optional;
26  import java.util.ServiceLoader;
27  
28  import javax.annotation.Nullable;
29  
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import com.cronutils.model.Cron;
34  import com.cronutils.model.CronType;
35  import com.cronutils.model.definition.CronDefinitionBuilder;
36  import com.cronutils.parser.CronParser;
37  import com.fasterxml.jackson.annotation.JsonCreator;
38  import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
39  import com.fasterxml.jackson.annotation.JsonInclude;
40  import com.fasterxml.jackson.annotation.JsonInclude.Include;
41  import com.fasterxml.jackson.annotation.JsonProperty;
42  import com.google.common.base.MoreObjects;
43  import com.google.common.collect.ImmutableList;
44  import com.google.common.collect.Streams;
45  
46  import com.linecorp.centraldogma.server.mirror.Mirror;
47  import com.linecorp.centraldogma.server.mirror.MirrorContext;
48  import com.linecorp.centraldogma.server.mirror.MirrorCredential;
49  import com.linecorp.centraldogma.server.mirror.MirrorDirection;
50  import com.linecorp.centraldogma.server.mirror.MirrorProvider;
51  import com.linecorp.centraldogma.server.storage.project.Project;
52  
53  // ignoreUnknown = true for backward compatibility since `type` field is removed.
54  @JsonIgnoreProperties(ignoreUnknown = true)
55  @JsonInclude(Include.NON_NULL)
56  public final class MirrorConfig {
57  
58      private static final Logger logger = LoggerFactory.getLogger(MirrorConfig.class);
59  
60      private static final String DEFAULT_SCHEDULE = "0 * * * * ?"; // Every minute
61  
62      private static final CronParser CRON_PARSER = new CronParser(
63              CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
64  
65      private static final List<MirrorProvider> MIRROR_PROVIDERS;
66  
67      static {
68          MIRROR_PROVIDERS = ImmutableList.copyOf(ServiceLoader.load(MirrorProvider.class));
69          logger.debug("Available {}s: {}", MirrorProvider.class.getSimpleName(), MIRROR_PROVIDERS);
70      }
71  
72      private final boolean enabled;
73      private final MirrorDirection direction;
74      @Nullable
75      private final String localRepo;
76      private final String localPath;
77      private final URI remoteUri;
78      @Nullable
79      private final String gitignore;
80      @Nullable
81      private final String credentialId;
82      private final Cron schedule;
83  
84      @JsonCreator
85      public MirrorConfig(@JsonProperty("enabled") @Nullable Boolean enabled,
86                          @JsonProperty("schedule") @Nullable String schedule,
87                          @JsonProperty(value = "direction", required = true) MirrorDirection direction,
88                          @JsonProperty(value = "localRepo", required = true) String localRepo,
89                          @JsonProperty("localPath") @Nullable String localPath,
90                          @JsonProperty(value = "remoteUri", required = true) URI remoteUri,
91                          @JsonProperty("gitignore") @Nullable Object gitignore,
92                          @JsonProperty("credentialId") @Nullable String credentialId) {
93          this.enabled = firstNonNull(enabled, true);
94          this.schedule = CRON_PARSER.parse(firstNonNull(schedule, DEFAULT_SCHEDULE));
95          this.direction = requireNonNull(direction, "direction");
96          this.localRepo = requireNonNull(localRepo, "localRepo");
97          this.localPath = firstNonNull(localPath, "/");
98          this.remoteUri = requireNonNull(remoteUri, "remoteUri");
99          if (gitignore != null) {
100             if (gitignore instanceof Iterable &&
101                 Streams.stream((Iterable<?>) gitignore).allMatch(String.class::isInstance)) {
102                 this.gitignore = String.join("\n", (Iterable<String>) gitignore);
103             } else if (gitignore instanceof String) {
104                 this.gitignore = (String) gitignore;
105             } else {
106                 throw new IllegalArgumentException(
107                         "gitignore: " + gitignore + " (expected: either a string or an array of strings)");
108             }
109         } else {
110             this.gitignore = null;
111         }
112         this.credentialId = credentialId;
113     }
114 
115     @Nullable
116     Mirror toMirror(Project parent, Iterable<MirrorCredential> credentials) {
117         if (!enabled || localRepo == null || !parent.repos().exists(localRepo)) {
118             return null;
119         }
120 
121         final MirrorContext mirrorContext = new MirrorContext(
122                 schedule, direction, findCredential(credentials, remoteUri, credentialId),
123                 parent.repos().get(localRepo), localPath, remoteUri, gitignore);
124         for (MirrorProvider mirrorProvider : MIRROR_PROVIDERS) {
125             final Mirror mirror = mirrorProvider.newMirror(mirrorContext);
126             if (mirror != null) {
127                 return mirror;
128             }
129         }
130 
131         throw new IllegalArgumentException("could not find a mirror provider for " + mirrorContext);
132     }
133 
134     private static MirrorCredential findCredential(Iterable<MirrorCredential> credentials, URI remoteUri,
135                                                    @Nullable String credentialId) {
136         if (credentialId != null) {
137             // Find by credential ID.
138             for (MirrorCredential c : credentials) {
139                 final Optional<String> id = c.id();
140                 if (id.isPresent() && credentialId.equals(id.get())) {
141                     return c;
142                 }
143             }
144         } else {
145             // Find by host name.
146             for (MirrorCredential c : credentials) {
147                 if (c.matches(remoteUri)) {
148                     return c;
149                 }
150             }
151         }
152 
153         return MirrorCredential.FALLBACK;
154     }
155 
156     @JsonProperty("enabled")
157     public boolean enabled() {
158         return enabled;
159     }
160 
161     @JsonProperty("direction")
162     public MirrorDirection direction() {
163         return direction;
164     }
165 
166     @Nullable
167     @JsonProperty("localRepo")
168     public String localRepo() {
169         return localRepo;
170     }
171 
172     @JsonProperty("localPath")
173     public String localPath() {
174         return localPath;
175     }
176 
177     @JsonProperty("remoteUri")
178     public String remoteUri() {
179         return remoteUri.toString();
180     }
181 
182     @JsonProperty("gitignore")
183     @Nullable
184     public String gitignore() {
185         return gitignore;
186     }
187 
188     @Nullable
189     @JsonProperty("credentialId")
190     public String credentialId() {
191         return credentialId;
192     }
193 
194     @JsonProperty("schedule")
195     public String schedule() {
196         return schedule.asString();
197     }
198 
199     @Override
200     public String toString() {
201         return MoreObjects.toStringHelper(this).omitNullValues()
202                           .add("enabled", enabled)
203                           .add("direction", direction)
204                           .add("localRepo", localRepo)
205                           .add("localPath", localPath)
206                           .add("remoteUri", remoteUri)
207                           .add("gitignore", gitignore)
208                           .add("credentialId", credentialId)
209                           .add("schedule", schedule)
210                           .toString();
211     }
212 }