src/main/java/com/google/cloud/security/scanner/actions/messengers/DiscrepancyAutoFixMessenger.java
public class DiscrepancyAutoFixMessenger
    extends DoFn<KV<GCPResource, Map<StateSource, GCPResourceState>>, String> {

  private final Aggregator<Long, Long> totalEnforcedStates = 
      createAggregator("Total enforced states", new Sum.SumLongFn());

  /**
   * Construct a notification message out of the incoming object.
   * The incoming object is a KV pair with the GCPResource as the key,
   * and a map containing the DESIRED and LIVE states.
   * The map will contain exactly 2 elements, corresponding to the two states.
   * @param processContext The ProcessContext object that contains processContext-specific methods and objects.
   * @exception Exception is thrown if the input is malformed or the desired state can't be set.
   */
  @Override
  public void processElement(ProcessContext processContext) throws Exception {
    GCPResource resource = processContext.element().getKey();
    Map<StateSource, GCPResourceState> input = processContext.element().getValue();
    if (input.size() == 2) {
      GCPResourceState liveState = input.get(StateSource.LIVE);
      GCPResourceState desiredState = input.get(StateSource.DESIRED);
      liveState.set(desiredState);
      String message = "Live state for resource " + resource.toString()+ " changed from:\n" +
          liveState.toString() + "to the desired state:\n" + desiredState.toString();
      processContext.output(message);
      totalEnforcedStates.addValue(1L);
    }
    else {
      throw new IllegalArgumentException(
          "The <StateSource, GCPResourceState> map does not contain exactly two elements."
      );
    }
  }
  
  public final Aggregator<Long, Long> getTotalEnforcedStatesAggregator() {
    return totalEnforcedStates;
  }
}
src/main/java/com/google/cloud/security/scanner/actions/extractors/FileToState.java
public class FileToState
    extends DoFn<KV<List<String>, String>, KV<GCPResource, GCPResourceState>> {

  private TupleTag<GCPResourceErrorInfo> errorOutputTag;

  public FileToState() {
  }

  public FileToState(TupleTag<GCPResourceErrorInfo> tag) {
    errorOutputTag = tag;
  }

  /**
   * Converts a Key-Value pair of (FilePath, FileContent) to (GCPResource, GCPResourceState).
   * The FilePath is a list of Strings which represents the location of a file.
   * The FileContent is the content of the file described by the FilePath.
   * The path is used to obtain the resource, and the content describes the state of that resource.
   * @param processContext The ProcessContext object that contains processContext-specific
   * methods and objects.
   */
  @Override
  public void processElement(ProcessContext processContext) {
    KV<List<String>, String> input = processContext.element();
    List<String> filePath = input.getKey();
    String fileContent = input.getValue();

    String orgName = filePath.size() > 0 ? filePath.get(0) : null;
    String projectId = filePath.size() > 1 ? filePath.get(1) : null;
    String policyFileName = filePath.size() > 2 ? filePath.get(2) : null;
    GCPProject project = new GCPProject(projectId, orgName);

    if (filePath.size() == 3 && GCPResourcePolicy.getPolicyFile().equals(policyFileName)) {
      // only project policies are supported for now.
      // filePath.size() must be 3 and of the form org_id/project_id/POLICY_FILE.
      Gson gson = new Gson();
      try {
        List<PolicyBinding> bindings = Arrays.asList(
            gson.fromJson(fileContent, PolicyBinding[].class));
        GCPResourceState policy = new GCPResourcePolicy(project, bindings);
        processContext.output(KV.of((GCPResource) project, policy));
        return;
      } catch (JsonSyntaxException jse) {
        addToSideOutput(
            processContext,
            project,
            String.format("Invalid policy json %s/%s/%s", orgName, projectId, policyFileName));
      }
    }
    addToSideOutput(
        processContext,
        project,
        String.format("Invalid policy filepath %s/%s/%s", orgName, projectId, policyFileName));
  }

  /**
   * Add some error output to the side output tag
   * @param context the ProcessContext for this DoFn
   * @param project the project associated with the error
   * @param errorMessage the message describing the error
   */
  private void addToSideOutput(ProcessContext context, GCPProject project, String errorMessage) {
    if (errorOutputTag != null) {
      context.sideOutput(errorOutputTag,
          new GCPResourceErrorInfo(project, errorMessage));
    }
  }
}
src/main/java/com/google/cloud/security/scanner/sources/LiveProjectSource.java
public class LiveProjectSource extends BoundedSource<GCPProject> {
  private static final long SIZE_ESTIMATE = 200;
  private static final String DELETE_PREFIX = "DELETE";
  private String orgId;

  /**
   * Constructor for LiveProjectSource.
   * @param orgId The ID of the organization from which we read the projects list.
   *  Set as null to read from all visible orgs.
   */
  public LiveProjectSource(String orgId) {
    this.orgId = orgId;
  }

  /**
   * Getter for the field orgId.
   * @return The ID of the organization whose projects are to be read.
   */
  public String getOrgId() {
    return this.orgId;
  }

  /**
   * This function just returns the same source as a list, and does not
   * actually split the load into several bundles.
   * @param desiredBundleSizeBytes The desired bundle size. Not used.
   * @param options Pipeline options. Not used
   * @return A list containing this source as its only element.
   */
  @Override
  public List<LiveProjectSource> splitIntoBundles(
      long desiredBundleSizeBytes, PipelineOptions options) throws Exception {
    List<LiveProjectSource> projectSources = new ArrayList<>(1);
    projectSources.add(this);
    return projectSources;
  }

  /**
   * Currently returns a hardcoded value.
   * Get the estimated size of the data that will be read by this source.
   * @param options Pipeline options. Not used.
   * @return The size estimate of the data to be read by this source.
   */
  @Override
  public long getEstimatedSizeBytes(PipelineOptions options) throws Exception {
    return SIZE_ESTIMATE;
  }

  /**
   * This function does not give any guarantees about whether its output will be sorted or not.
   * @param options Pipeline options. Not used.
   * @return Returns true if the source's output is always sorted. False otherwise.
   */
  @Override
  public boolean producesSortedKeys(PipelineOptions options) throws Exception {
    return false;
  }

  /**
   * Create a new reader that will read from this source.
   * @param options Pipeline options. Not used.
   * @return A BoundedReader object to read from this source.
   */
  @Override
  public BoundedReader<GCPProject> createReader(PipelineOptions options) throws IOException {
    return new LiveProjectReader(this);
  }

  /**
   * Validate whether this source can function or not.
   */
  @Override
  public void validate() {}

  /**
   * Get the default coder to use for this source's output.
   * @return The default coder to use for this source's output.
   */
  @Override
  public Coder<GCPProject> getDefaultOutputCoder() {
    return SerializableCoder.of(GCPProject.class);
  }

  /** Reader for LiveProjectSource */
  public static class LiveProjectReader extends BoundedReader<GCPProject> {
    private LiveProjectSource source;
    private List<GCPProject> projects;
    private String nextPageToken;

    /**
     * Getter for the projects field.
     * @return Return the projects currently in the reader's queue.
     */
    List<GCPProject> getProjects() {
      return projects;
    }

    /**
     * Getter for the nextPageToken field.
     * @return Return the nextPageToken to be used in the
     * next request to get the next page of projects.
     */
    String getNextPageToken() {
      return nextPageToken;
    }

    /**
     * Construct a LiveProjectReader object.
     * @param source The source this reader is supposed to read from.
     */
    public LiveProjectReader(LiveProjectSource source) {
      this.source = source;
      this.projects = new ArrayList<>();
      this.nextPageToken = null;
    }

    /**
     * Initialize the reader so it can begin reading files.
     * @return True if the initialization succeeded. False otherwise.
     */
    @Override
    public boolean start() throws IOException {
      return refreshProjects(null);
    }

    /**
     * Advance the reader so it can read the next file in the queue.
     * @return True if there are still more files to be read. False otherwise.
     */
    @Override
    public boolean advance() throws IOException {
      if (this.projects.size() > 1) {
        this.projects.remove(0);
        return true;
      } else if (this.projects.size() == 1) {
        this.projects.remove(0);
        // if token is null the last page read was the last page to be read.
        return (this.nextPageToken != null) && refreshProjects(this.nextPageToken);
      }
      return false;
    }

    /**
     * Get the next file in queue.
     * @return A GCPProject object representing the project that is read.
     * @throws NoSuchElementException If the file can't be read from the CRM API.
     */
    @Override
    public GCPProject getCurrent() throws NoSuchElementException {
      if (this.projects.isEmpty()) {
        throw new NoSuchElementException("No more GCPProject objects");
      }
      return this.projects.get(0);
    }

    /**
     * Close this reader.
     */
    @Override
    public void close() throws IOException {}

    /**
     * Return the source this reader is reading from.
     * @return The source this reader is reading from.
     */
    @Override
    public BoundedSource<GCPProject> getCurrentSource() {
      return this.source;
    }

    private boolean refreshProjects(String nextPageToken) throws IOException {
      ListProjectsResponse projectListResponse;
      Projects.List projectsList;
      try {
        projectsList = GCPProject.getProjectsApiStub().list();
        if (nextPageToken != null) {
          projectsList = projectsList.setPageToken(nextPageToken);
        }
        if (source.getOrgId() != null) {
            projectsList = projectsList
                .setFilter("parent.type:organization parent.id:" + source.getOrgId());
        }
        projectListResponse = projectsList.execute();
      } catch (GeneralSecurityException gse) {
        throw new IOException("Cannot get projects. Access denied");
      }
      List<Project> projects = projectListResponse.getProjects();

      for (Project project : projects) {
        String orgId = null;
        if (project.getParent() != null) {
          orgId = project.getParent().getId();
        }
        if (project.getLifecycleState() == null
            || project.getLifecycleState().startsWith(DELETE_PREFIX)) {
          continue;
        }
        this.projects.add(new GCPProject(project.getProjectId(), orgId, project.getName()));
      }
      this.nextPageToken = projectListResponse.getNextPageToken();

      return !this.projects.isEmpty();
    }
  }
}
src/main/java/com/google/cloud/security/scanner/actions/extractors/ExtractState.java
public class ExtractState
  extends DoFn<GCPProject, KV<GCPResource, GCPResourceState>> {

  private TupleTag<GCPResourceErrorInfo> errorOutputTag;
  private transient static final Logger logger = Logger.getLogger(ExtractState.class.getName());

  public ExtractState() {
  }

  public ExtractState(TupleTag<GCPResourceErrorInfo> tag) {
    errorOutputTag = tag;
  }

  /**
   * Convert a GCPProject to a Key-Value pair of the project and its policy.
   * @param processContext The ProcessContext object that contains processContext-specific
   * methods and objects.
   */
  @Override
  public void processElement(ProcessContext processContext) {
    GCPProject input = processContext.element();

    if (input.getId() == null) {
      this.addToSideOutput(processContext, input, "Null project id");
      return;
    }
    GCPResourceState policy = null;
    String errorMsg = null;
    try {
      policy = input.getPolicy();
    } catch (Exception e) {
      errorMsg = e.getMessage();
      logger.log(Level.FINE, "Error getting policy", e);
    }

    if (policy == null) {
      this.addToSideOutput(processContext, input, String.format("Policy error %s", errorMsg));
    } else {
      processContext.output(KV.of((GCPResource) input, policy));
    }
  }

  /**
   * Add some error output to the side output tag
   * @param context the ProcessContext for this DoFn
   * @param project the project associated with the error
   * @param errorMessage the message describing the error
   */
  private void addToSideOutput(ProcessContext context, GCPProject project, String errorMessage) {
    if (errorOutputTag != null) {
      context.sideOutput(errorOutputTag,
          new GCPResourceErrorInfo(project, errorMessage));
    }
  }
}
src/main/java/com/google/cloud/security/scanner/primitives/GCPProject.java
public class GCPProject implements PoliciedObject {
  private static Projects projectApiStub;

  private static final RateLimiter rateLimiter = CloudUtil.getAdminApiRateLimiter();

  private String name;
  private String id;
  private String orgId;

  /**
   * Setter for the CRM Projects API stub.
   * @param project The CRM Projects API stub.
   */
  public static synchronized void setProjectsApiStub(Projects project) {
    GCPProject.projectApiStub = project;
  }

  /**
   * Return the Projects api object used for accessing the Cloud Resource Manager Projects API.
   * @return Projects api object used for accessing the Cloud Resource Manager Projects API
   * @throws GeneralSecurityException Thrown if there's a permissions error.
   * @throws IOException Thrown if there's an IO error initializing the API object.
   */
  public static synchronized Projects getProjectsApiStub()
      throws GeneralSecurityException, IOException {
    if (projectApiStub != null) {
      return projectApiStub;
    }
    HttpTransport transport;
    GoogleCredential credential;
    JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
    transport = GoogleNetHttpTransport.newTrustedTransport();
    credential = GoogleCredential.getApplicationDefault(transport, jsonFactory);
    if (credential.createScopedRequired()) {
      Collection<String> scopes = CloudResourceManagerScopes.all();
      credential = credential.createScoped(scopes);
    }
    projectApiStub = new CloudResourceManager
        .Builder(transport, jsonFactory, credential)
        .build()
        .projects();
    return projectApiStub;
  }

  /**
   * Construct a GCPProject object with only the project ID.
   * Other fields are left as null.
   * @param id The ID of the project. This is different from the project name.
   * @throws IllegalArgumentException If the project id is null.
   */
  public GCPProject(String id) throws IllegalArgumentException {
    this(id, null, null);
  }

  /**
   * Construct a GCPProject object with the project and org IDs.
   * @param id The ID of the project. This is different from the project name.
   * @param orgId The ID of the org this project belongs to.
   * @throws IllegalArgumentException If the project id is null.
   */
  public GCPProject(String id, String orgId) throws IllegalArgumentException {
    this(id, orgId, null);
  }

  /**
   * Construct a GCPProject object with the project and org IDs, and the project name.
   * @param id The id of the project.
   * @param orgId The id of the org this project belongs to.
   * @param name The display name of the project.
   * @throws IllegalArgumentException If the project id is null.
   */
  public GCPProject(String id, String orgId, String name) throws IllegalArgumentException {
    if (id == null) {
      throw new IllegalArgumentException("GCPProject's id cannot be null");
    }
    if (orgId == null) {
      orgId = "";
    }
    if (name == null) {
      name = "";
    }
    this.id = id;
    this.orgId = orgId;
    this.name = name;
  }

  /**
   * @return The display name of the project.
   */
  public String getName() {
    return this.name;
  }

  /**
   * @return The ID of the org this project belongs to.
   */
  public String getOrgId() {
    return this.orgId;
  }

  /**
   * @return The ID of the project.
   */
  @Override
  public String getId() {
    return this.id;
  }

  /**
   * Get the IAM policy binding this project.
   * @return The GCPResourcePolicy object which represents the policy binding this project.
   * @throws IOException Thrown if there's an IO error reading the policy.
   * @throws GeneralSecurityException Thrown if there's a permissions error reading the policy.
   */
  @Override
  public GCPResourcePolicy getPolicy() throws IOException, GeneralSecurityException {
    Map<String, List<String>> bindings = new HashMap<>();
    Policy policy = null;

    rateLimiter.acquire();

    try {
      policy = getProjectsApiStub()
          .getIamPolicy(this.id, new GetIamPolicyRequest())
          .execute();
    } catch (GeneralSecurityException gse) {
      throw new GeneralSecurityException(
          "Cannot fetch IAM policy for project + " + id + "\nMessage: " + gse.getMessage()
      );
    }
    if (policy != null && policy.getBindings() != null) {
      for (Binding binding : policy.getBindings()) {
        bindings.put(binding.getRole(), binding.getMembers());
      }
      return new GCPResourcePolicy(this, bindings);
    }
    return null;
  }

  /**
   * Set the IAM policy that will bind this project. This requires the "Project Owner"
   * role to be added to the service account.
   * @param sourceGCPResourcePolicy The policy this project is to be bound to.
   * @throws IOException Thrown if there's an IO error setting the policy.
   * @throws GeneralSecurityException Thrown if there's a permissions error setting the policy.
   */
  @Override
  public void setPolicy(GCPResourcePolicy sourceGCPResourcePolicy)
      throws IOException, GeneralSecurityException {
    Policy policy = new Policy();
    List<Binding> bindings = new ArrayList<>(sourceGCPResourcePolicy.getBindings().size());
    SetIamPolicyRequest request = new SetIamPolicyRequest();

    rateLimiter.acquire();

    for (PolicyBinding sourceBinding : sourceGCPResourcePolicy.getBindings()) {
      Binding binding = new Binding();
      binding.setRole(sourceBinding.getRole());
      binding.setMembers(sourceBinding.getMembers());
      bindings.add(binding);
    }
    policy.setBindings(bindings);
    request.setPolicy(policy);
    getProjectsApiStub().setIamPolicy(this.id, request).execute();
  }

  /**
   * Comparator for GCPProject objects. It only checks for equality of the project IDs.
   * @param o The object to compare with.
   * @return True if they have the same project ID, false otherwise.
   */
  @Override
  public boolean equals(Object o) {
    return (o instanceof GCPProject) && ((GCPProject) o).id.equals(this.id);
  }

  /**
   * hashCode method which only respects the project's id.
   * @return Hash code for this object.
   */
  @Override
  public int hashCode() {
    return this.id.hashCode();
  }

  /**
   * toString method which only returns the id of the project.
   * @return The id of the project.
   */
  @Override
  public String toString() {
    return this.id;
  }
}
src/main/java/com/google/cloud/security/scanner/actions/modifiers/FilterOutMatchingState.java
public class FilterOutMatchingState
    extends DoFn<KV<GCPResource, KV<StateSource, GCPResourceState>>,
    KV<GCPResource, Map<StateSource, GCPResourceState>>> {

  private PCollectionView<Map<GCPResource, KV<StateSource, GCPResourceState>>> view;

  /**
   * Constructor for the FilterOutMatchingState DoFn.
   * @param view The PCollectionView which contains the side-input elements.
   */
  public FilterOutMatchingState
      (PCollectionView<Map<GCPResource, KV<StateSource, GCPResourceState>>> view) {
    this.view = view;
  }

  /**
   * Process an element of the type KV<GCPResource, KV<StateResource, GCPResourceState>>
   * and output only those states that do not match.
   * The GCPResource is the resource that is being described by the GCPResourceState. In
   * this case, it's the GCP project.
   * The GCPResourceState is the attribute describing the GCPResource, i.e. the project policies.
   * StateSource represents the source of the GCPResourceState:
   *  - it was either checked in as a known-good, or
   *  - it is the live state of the resource
   *  GCPResourceStates tagged with one StateSource (say, DESIRED) will be inputted through
   *  a side input, and those tagged with the other will be inputted through the main input.
   * @param context The ProcessContext object that contains context-specific methods and objects.
   */
  @Override
  public void processElement(ProcessContext context) {
    // the project
    GCPResource resource = context.element().getKey();
    // the project's policies
    KV<StateSource, GCPResourceState> mainValue = context.element().getValue();

    // if the known-good policies' projects contain this project...
    if (context.sideInput(this.view).containsKey(resource)) {
      // make sure there's an element in the side input with the same GCPResource.

      KV<StateSource, GCPResourceState> sideValue = context.sideInput(this.view).get(resource);
      if (!mainValue.getValue().equals(sideValue.getValue())) {
        // make sure the GCPResourceStates are different.

        // the HashMap will contain two entries, one for
        // the DESIRED state and one for the LIVE state.
        Map<StateSource, GCPResourceState> mismatchedStates = new HashMap<>(2);
        mismatchedStates.put(mainValue.getKey(), mainValue.getValue());
        mismatchedStates.put(sideValue.getKey(), sideValue.getValue());
        context.output(KV.of(resource, mismatchedStates));
      }
    }
  }
}
src/main/java/com/google/cloud/security/scanner/actions/modifiers/TagStateWithSource.java
public class TagStateWithSource
  extends DoFn<KV<GCPResource, GCPResourceState>,
        KV<GCPResource, KV<StateSource, GCPResourceState>>> {

  /**
   * Represents the source of a given GCPResourceState.
   */
  public enum StateSource {
    // LIVE: The current state of the GCPResource.
    LIVE,
    // DESIRED: The ideal state of the GCPResource.
    DESIRED
  }

  private StateSource source;

  /**
   * Set the StateSource tag this transform will tag all the inputs with.
   * @param source The tag to be applied to all the inputs
   */
  public TagStateWithSource(StateSource source) {
    this.source = source;
  }

  /**
   * Tag all incoming inputs with the StateSource variable set in this transform.
   * @param processContext The ProcessContext object that contains processContext-specific
   * methods and objects.
   */
  @Override
  public void processElement(ProcessContext processContext) {
    KV<GCPResource, GCPResourceState> input = processContext.element();
    processContext.output(KV.of(input.getKey(), KV.of(this.source, input.getValue())));
  }
}
src/main/java/com/google/cloud/security/scanner/primitives/GCPResourceState.java
public interface GCPResourceState extends Serializable {
  /** Change the live version of this resource */
  public void set(GCPResourceState state) throws Exception;

  /** Get the live version of this resource */
  public GCPResourceState get() throws Exception;
}
src/main/java/com/google/cloud/security/scanner/actions/modifiers/TagStateWithSource.java
public enum StateSource {
  // LIVE: The current state of the GCPResource.
  LIVE,
  // DESIRED: The ideal state of the GCPResource.
  DESIRED
}
src/main/java/com/google/cloud/security/scanner/primitives/GCPResource.java
public interface GCPResource extends Serializable {

  /**
   * Return the ID associated with this resource.
   * @return A string representation of this resource's ID.
   */
  public abstract String getId();
}


Question: import com.google.cloud.dataflow.sdk.Pipeline;
import com.google.cloud.dataflow.sdk.PipelineResult;
import com.google.cloud.dataflow.sdk.io.BoundedSource;
import com.google.cloud.dataflow.sdk.io.Read;
import com.google.cloud.dataflow.sdk.options.PipelineOptions;
import com.google.cloud.dataflow.sdk.runners.AggregatorRetrievalException;
import com.google.cloud.dataflow.sdk.runners.AggregatorValues;
import com.google.cloud.dataflow.sdk.testing.DataflowAssert;
import com.google.cloud.dataflow.sdk.transforms.PTransform;
import com.google.cloud.dataflow.sdk.transforms.ParDo;
import com.google.cloud.dataflow.sdk.transforms.View;
import com.google.cloud.dataflow.sdk.values.KV;
import com.google.cloud.dataflow.sdk.values.PCollection;
import com.google.cloud.dataflow.sdk.values.PCollectionView;
import com.google.cloud.dataflow.sdk.values.PDone;
import com.google.cloud.security.scanner.actions.extractors.ExtractState;
import com.google.cloud.security.scanner.actions.extractors.FileToState;
import com.google.cloud.security.scanner.actions.messengers.DiscrepancyAutoFixMessenger;
import com.google.cloud.security.scanner.actions.modifiers.FilterOutMatchingState;
import com.google.cloud.security.scanner.actions.modifiers.TagStateWithSource;
import com.google.cloud.security.scanner.actions.modifiers.TagStateWithSource.StateSource;
import com.google.cloud.security.scanner.primitives.GCPProject;
import com.google.cloud.security.scanner.primitives.GCPResource;
import com.google.cloud.security.scanner.primitives.GCPResourceState;
import com.google.cloud.security.scanner.sources.LiveProjectSource;
import java.util.List;
import java.util.Map;
/**
 * Copyright 2016 Google Inc.
 *
 * Licensed 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.google.cloud.security.scanner.pipelines;


/**
 * Check the live state of GCP resources and compare them to a known-good.
 */
public class DesiredStateEnforcer {
  private Pipeline pipeline;
  private PCollection<String> outputMessages;
  private static DiscrepancyAutoFixMessenger discrepancyAutoFixMessenger =
      new DiscrepancyAutoFixMessenger();
  private long enforcedStates;

  /**
   * Construct a LiveStateChecker to compare the live states of GCP resources
   * with their checked-in known-good counterparts.
   * @param options The options used to construct the pipeline.
   * @param knownGoodSource The source used to read the known-good.
   * @param org The organization the projects are to be read from.
   */
  public DesiredStateEnforcer(PipelineOptions options,
      BoundedSource<KV<List<String>, String>> knownGoodSource,
      String org) {
    this.pipeline = Pipeline.create(options);
    this.outputMessages = constructPipeline(this.pipeline, org, knownGoodSource);
    this.enforcedStates = 0L;
  }

  /**
   * Run the pipeline.
   * @throws AggregatorRetrievalException
   */
  public DesiredStateEnforcer run() throws AggregatorRetrievalException {
    PipelineResult result = this.pipeline.run();

    AggregatorValues<Long> aggregatorValues = result.getAggregatorValues(
            discrepancyAutoFixMessenger.getTotalEnforcedStatesAggregator());
    this.enforcedStates =
        aggregatorValues.getTotalValue(
            discrepancyAutoFixMessenger.getTotalEnforcedStatesAggregator().getCombineFn());
    return this;
  }

  public long getTotalEnforcedStates() {
    return this.enforcedStates;
  }

  /**
   * Attach a sink to output the messages to.
   * @param sinkTransform Transform to output messages to the sink.
   */
  public DesiredStateEnforcer attachSink(PTransform<PCollection<String>, PDone> sinkTransform) {
    this.outputMessages.apply(sinkTransform);
    return this;
  }

  /**
   * Assert a containsInAnyOrder on the pipeline with the passed in inputs.
   * @param expectedOutputs The strings that are to be contained in the pipeline's final output.
   */
  DesiredStateEnforcer appendAssertContains(String[] expectedOutputs) {
    DataflowAssert.that(this.outputMessages).containsInAnyOrder(expectedOutputs);
    return this;
  }

  private PCollection<String> constructPipeline(Pipeline pipeline, String org,
      BoundedSource<KV<List<String>, String>> knownGoodSource) {
    // Read files from GCS.
    PCollection<KV<List<String>, String>> knownGoodFiles =
        pipeline.apply("Read known-good data", Read.from(knownGoodSource));
    // Convert files to GCPResourceState objects.
    PCollection<KV<GCPResource, GCPResourceState>> knownGoodStates =
        knownGoodFiles.apply(ParDo.named("Convert file data to Java Objects")
            .of(new FileToState()));
    // Tag the state objects to indicate they're from a checked-in repo and not live.
    PCollection<KV<GCPResource, KV<StateSource, GCPResourceState>>> taggedKnownGoodStates =
        knownGoodStates.apply(ParDo.named("Mark states as being known-good")
            .of(new TagStateWithSource(StateSource.DESIRED)));

    // Read projects from the CRM API.
Answer:     PCollection<GCPProject> allProjects =