Tuesday, September 20, 2022

Deploying of Sitecore JSS (Next.js) application to Vercel from on-prem Gitlab

 I just went through implementing of on-prem Gitlab CI/CD pipeline for deploying of a Next.js Sitecore JSS application to Vercel. At first everything looked straight forward, but there a few hiccups down the road sharing of which I thought would be beneficial to others.

Building and deploying of Next.js Sitecore JSS application

Vercel team referred us to the following documentation to help use configure on-prem Gitlab CI/CD pipeline to deploy the application to Vercel:

I followed the instructions and configured the CI/CD pipeline script to run the following commands:

- npm install --global vercel 
- vercel pull --yes --environment=preview --token=$VERCEL_TOKEN 
- vercel build --token=$VERCEL_TOKEN 
- vercel deploy --prebuilt --token=$VERCEL_TOKEN

I also added five environment variables to the Vercel project:
Vercel environment variables

When I ran the pipeline and Vercel CLI commands finished executing, I saw the following output in the deployment screen capture. The cause of this broken output was the broken references to JS and CSS files. The host name in these references are pointing to a local instance. If I added PUBLIC_URL to the environment variables for all environments, then the references would be correct and the application would load properly. However, if different environments are expected to have different PUBLIC_URL this solution wouldn't work.



After digging a little deeper into Sitecore JSS code and debugging the deployment script that Vercel CLI produced I realized that VERCEL_URL is always empty when you run the build "locally" (not in Vercel deployment). Going through documentation didn't help much.

By pure accident I ran a command to switch scopes:

vercel --scope $VERCEL_SCOPE ${VERCEL_PARAMS} --token=${VERCEL_TOKEN} 

And all of a sudden I see a traditional build starting in Vercel. It goes through the usual build, checks and deployment and I see JS and CSS references getting proper hostname in references.

I filed a support ticket with Vercel asking to explain the behavior and advise on the way forward. After three weeks of back and forth they finally explained why changing of scope triggered a build. 

Apparently any run of vercel command triggers a build and deployment, and if you run this CLI command from your local "rendering" application, it will trigger a build using your local application. 

The bottom line is that for Sitecore JSS applications and for any applications that expect using VERCEL_URL during build you can't use pull/build/deploy. You have to use vercel command. A

Deployment URL


If you need to get a value of your Vercel deployment each time you run CLI build and deployment command, you need to use the following format:

    - DEPLOYMENT_URL=$(vercel --scope $VERCEL_SCOPE ${VERCEL_PARAMS} --token=${VERCEL_TOKEN} --prod)
    - echo $DEPLOYMENT_URL

However, if you are trying to assign an alias to a preview or development deployment don't expect it to work. Alias assignment only works for production deployments.

Final Script

After all troubles I ended up creating a separate Vercel project for each environment. Each branch from our on-prem Gitlab was configured to deploy to a separate Vercel project into production environment. We can still deploy to preview from branches, but the main environment branch was configured to always deploy to production. That allowed us to have specific domain names for each environment branch. If we were using cloud version of Gitlab, we probably wouldn't have to do that, but since we are working with on-prem version, there was no other choice.

So the final Vercel portion of the script looks like this:

    - npm install --global vercel
    - DEPLOYMENT_URL=$(VERCEL_ORG_ID=$VERCEL_ORG_ID VERCEL_PROJECT_ID=$VERCEL_DEV_PROJECT_ID vercel --scope $VERCEL_SCOPE ${VERCEL_PARAMS} --token=${VERCEL_TOKEN} --prod)
    - echo $DEPLOYMENT_URL
 

Other considerations

If you are working with on-prem source control and have to use Vercel CLI, make sure that you are running the build on linux/amd64 machine. The system must match Vercel platform Build Image (https://vercel.com/docs/build-output-api/v3#introduction/known-limitations)

Friday, May 27, 2022

Vercel, Next.js middleware and personalization using Sitecore Personalize

When I started working with Vercel middleware I was convinced that I would be able to parse html before returning it to the user, however, to my big surprise you can't do html parsing in middleware which makes complete sense from the caching perspective.

I used Boxever library implementation from Sitecore.Demo.Edge Git repo and added a few things that I needed.

So if html can't be parse, how would one implement personalization? While searching for solution I ran into a YouTube video by Steve Sewel and a solution from https://www.builder.io. The solution that Steve described made perfect sense and seemed to be the only way to implement personalization in middleware. I used the same approach in my POC and here what I was able to do.

Use case

A user from Germany comes to the site and a component on a product listing page shows a greeting stating the user is visiting from Germany.

While the use case is very simple and you don't necessarily need Sitecore Personalize to implement that, it was the easiest scenario to implement to test the waters with middleware.

Solution

Under /pages folder I added _middleware.ts file which mean that the middleware function will be called for all pages. Here is the content of the file:

import { getPersonalizedPagePath } from 'lib/middleware-utils';
import { NextResponse, NextRequest } from 'next/server'

const excludededPrefixes = ['/favicon', '/api']

export async function middleware(req: NextRequest) {
  const url = req.nextUrl.clone();
  const country = req.geo?.country || 'US';

  if(!excludededPrefixes.find((path) => url.pathname?.startsWith(path))) {  
    const browserId = req.cookies[`bid_${process.env.CDP_CLIENT_KEY}`] || process.env.CDP_BROWSER_ID;
    const friendlyId = 'user_geolocation';  

    if (!url?.pathname!.includes(';')) {        
      const rewriteUrl = await getPersonalizedPagePath(friendlyId, url?.pathname!, req.cookies, country, browserId);
      if (rewriteUrl && rewriteUrl.path) {
        url.pathname = rewriteUrl.path;          
        return NextResponse.rewrite(url).cookie(`bx_userAttributes_${friendlyId}`, rewriteUrl[friendlyId]);        
      }
    }

    return NextResponse.rewrite(url).cookie(`bx_userAttributes_${friendlyId}`, friendlyId);    
  }
  return NextResponse.rewrite(url);  
}

As you can see I am rewriting the url of the current page to include country and friendlyId parameters.

If a request is made to /products page, for the user from Germany the rewrite url will look like this:

/products;country=DE;user_geolocation=Germany

Country code is easily retrieved from NextRequest. The second parameter is a little bit more involved. I defined a Full Stack experience in Sitecore Personalize that is called user_geolocation. The experience returns Germany if the latest guest session come from Germany. I added the country code (retrieved from NextRequest) to the VIEW event, so now all guest VIEW events have a country code. I also have a Decision Model attached to my Full Stack experience which gives me flexibility with API response. I can return "Germany" or I can return something else that I can use to display personalized content. The only thing to be aware of is the length of the outcome. Since it is part of the url, it should be short. I can use this response either to hardcode personalization or retrieve personalized content from Sitecore. For the simplicity of this POC, I hardcoded the personalization message.

When the content for rewritten url is being request and for SSG Next.js application it is possible because of the Next.js ISR functionality. The fallback configuration is set to "blocking" as for a usual Sitecore JSS application with SSG and the revalidate for this POC is set to 15 seconds. In real live you would want it to be as high as possible.

When the new version of the pages is being generated getStaticProps function is being called in [[...path]].ts. I added a few lines to read the parameters that I set in middleware and remove them from the actual page path, so it can retrieve the proper page from Sitecore.

export const getStaticProps: GetStaticProps = async (context) => {
  if (context) {    
    let userAttributes:any = {};
    let path:string[] = [];
    if(context.params?.path) {
      (context.params?.path as string[]).forEach((part) => {
        if (part && part.includes(';')) {
          userAttributes = { ...getTargetingValues(part?.split(';').slice(1)) };
          path.push(part?.split(';')[0]);
        } else {
          path.push(part);
        }
      });
    }
    context.params!.userAttributes = userAttributes;
    context.params!.path = path;
  }

  const props = await sitecorePagePropsFactory.create(context);  

  return {
    props,
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 15 seconds
    revalidate: 15, // In seconds
    notFound: props.notFound, // Returns custom 404 page with a status code of 404 when true
  };
};

Now in the Next.js component that will render the personalized content I added the following:

import { Field, withDatasourceCheck, useComponentProps, GetStaticComponentProps } from '@sitecore-jss/sitecore-jss-nextjs';
import { StyleguideComponentProps } from 'lib/component-props';

type PersonalizedNavigationProps = StyleguideComponentProps & {
  fields: {
    friendlyId: Field<string>;
  };
};

type PersonalizedNavigationData = { content: '', country: '' };

const PersonalizedNavigation = ({ rendering }: PersonalizedNavigationProps): JSX.Element => {
  const personalizedContent = rendering.uid ? useComponentProps<PersonalizedNavigationData>(rendering.uid) : undefined;
  let personalizedOutput = '';
  //console.log(personalizedContent);
  if (personalizedContent?.country) {    
    switch(personalizedContent?.country) {
      case 'US': {
        personalizedOutput = 'USA';
        break;
      }
      case 'DE': {
        personalizedOutput = 'Germany';
        break;
      }
      default: {
        //statements;
        break;
      }
    }
  }

  if (personalizedContent?.content) {    
    switch(personalizedContent?.content) {
      case 'US': {
        personalizedOutput = personalizedOutput + ' ::: USA';
        break;
      }
      case 'DE': {
        personalizedOutput = personalizedOutput + ' ::: Germany';
        break;
      }
      default: {
        //statements;
        break;
      }
    }
  }

  return (
    <div>
      <h2>
        Country:
        {personalizedOutput && (
          <> {personalizedOutput}</>
        )}      
      </h2>
    </div>
  );
};

export const getStaticProps: GetStaticComponentProps = async (rendering:any, context:any, params:any) => {
  if (params && rendering && context) {
    const friendlyId = rendering?.fields?.data?.contextItem?.friendlyId.value || 'user_geolocation' as string;
    const country = params?.params?.userAttributes ? params?.params?.userAttributes?.country : '';
    const output = params?.params?.userAttributes ? params?.params?.userAttributes[friendlyId] : '';
    //console.log(params);
    return { country: country || '', content: output || ''};
  }
  return {};
};

export default withDatasourceCheck()<PersonalizedNavigationProps>(PersonalizedNavigation);

And on the page after the code has been deployed to Vercel the following snippet will render:




Below you'll find the files references from the code above. I decided to separate them into a separate section to avoid the flow disruption in my explanation.

Referenced .ts files:

middleware-utils.ts

import { DynamicData, getDynamicData, PersonalizedPath } from "src/services/BoxeverApiService";

const getUrlSegments = (values:any) => {
    const attrs = Object.keys(values);
    return attrs
        .map((attr) => `${attr}=${encodeURIComponent(values[attr])}`)
        .sort();
};

const getTargetingCookies = (cookiesMap:any) => Object.keys(cookiesMap).filter((cookie) => cookie.startsWith("bx_userAttributes"));

export function getTargetingValues(path:any) {
    return path.sort().reduce((acc:any, segment:any) => {
        const [key, value] = segment.split("=");
        if (key) {
            return Object.assign(Object.assign({}, acc), { [key]: decodeURIComponent(value) });
        }
        return acc;
    }, {});
};

export function getPersonalizedRewrite(pathname: string, cookies: Record<string, string>, country: string, experiencePair: string) {
    const attributes = getTargetingCookies(cookies);
    const values = attributes.reduce((acc, cookie) => {
        const value:string = cookies[cookie];
        const key = cookie.split("bx_userAttributes_")[1];
        return Object.assign(Object.assign({}, acc), (typeof value === 'string' && { [key]: value }));
    }, {});
    if (Object.keys(values).length > 0 || country) {
        return `${pathname};${experiencePair};${getUrlSegments(Object.assign(values, { country })).join(";")}`;
    }
    return false;
}

export function getUserAttribute(name:string, path:string) {
    if (name && path) {
        const attr = path.split(';');
        let attrValue = '';
        attr.forEach((part) => {
            const pathAttr = part.split('=');
            if (pathAttr[0] === name) {
                attrValue = pathAttr[1];
            }
        });

        return attrValue;
    }

    return '';
}

export async function getPersonalizedPagePath(friendlyId:string, path:string, cookies:any, country:string, browserId?:string):Promise<PersonalizedPath> {
    let response:PersonalizedPath = {};
    const outputValue = cookies[`bx_userAttributes_${friendlyId}`];
    if (browserId) {
        await getDynamicData('en', friendlyId, browserId).then((content: DynamicData) => {
            const result = content && content.decisionValues ? content.decisionValues.filter((i) => { return i.decisionType === 'programmable'})[0].outputValue : ''
            if (result) {    
                response[friendlyId] = result;
                response.path = getPersonalizedRewrite(
                    path,
                    cookies,
                    country,
                    `${friendlyId}=${response[friendlyId]}`
                ) as string;

                return response;
            }      
            return response;
        });
    } else if (outputValue) {
        response[friendlyId] = outputValue;
        response.path = getPersonalizedRewrite(
            path,
            cookies,
            country,
            `${friendlyId}=${response[friendlyId]}`
        ) as string;

        return response;
    }
   
    return response;
}

BoxeverApiService.ts

import BoxeverServiceConfig from './BoxeverApiServiceConfig';

// ***** TYPES *****

type GuestRef = string;

interface GuestRefResponse {
  guestRef: GuestRef;
}


// ***** API *****

const CDP_CLIENT_KEY = process.env.CDP_CLIENT_KEY || '';
const CDP_API_TARGET_ENDPOINT = process.env.CDP_API_TARGET_ENDPOINT || '';
const CDP_API_ENDPOINT = process.env.CDP_API_ENDPOINT || '';
export const isCdpConfigured = !!CDP_CLIENT_KEY && !!CDP_API_TARGET_ENDPOINT;

// ****************************************************************************
// Delaying calls is required due to some race conditions where a guest
// identification call was executed before the Boxever library had finished
// initializing.
// ****************************************************************************

function callFlows(flowConfig: Record<string, unknown>) {
  return new Promise(function (resolve, reject) {
    try {
      boxeverGet('callFlows', flowConfig)
      .then((result) => { resolve(result) })
      .catch((error) => console.log(error));      
    } catch (err) {
      reject(err);
    }
  });
}

// ****************************************************************************
// Gets the current guest ref by calling a CDP flow.
// This is a workaround needed when an anonymous guest identify itself as a
// previously known guest, CDP is merging the 2 guests. The browser keeps the
// anonymous guest ref. From that moment, calls to the authenticated CDP APIs
// like get/set for data extensions are failing when using this old anonymous
// guest ref. These calls require the previously known guest ref which is
// returned by our flow.
// ****************************************************************************
export function getGuestRef(): Promise<GuestRefResponse> {
  return callFlows({
    friendlyId: 'get_guestref',
  }) as Promise<GuestRefResponse>;
}

export async function getBrowserId() {
  let browserId = '';
  const url = `${CDP_API_TARGET_ENDPOINT}/browser/create.json?client_key=${BoxeverServiceConfig.clientKey}?message={}`;
  const response = await fetch(url, { headers: {
    'Content-Type': 'application/json',
  } });
  response.json().then((response) => { browserId = response.ref || '' }).catch((error) => console.log(error));
  return browserId;
}

async function boxeverGet(action: string, payload?: Record<string, unknown>): Promise<unknown> {  
  let message = {...BoxeverServiceConfig, ...payload};
  const url = `${CDP_API_ENDPOINT}/${action}?message=${JSON.stringify(message)}`;
  const response = await fetch(url, { headers: {
    'Content-Type': 'application/json',
  } });
  return response.json().catch((error) => console.log(error));      ;
}

export interface DynamicData {
  decisionValues: [
    {
      decisionType: string,
      trackingUrl: string,
      decisionName: string,
      outputName: string,
      outputValue: string,
    }
  ],
  decisionOffers: [
    {
      ref: string,
      name: string,
      description: string,
      status: string,
      attributes: {
        link: string,
        name: string,
        imageUrl: string
      }
    }
  ]
}

export interface UserAttributes {
  urlPath:string,
  countr:string,
  browserId:string
}

export interface CreateResponse {
  status: string,
  version: string,
  client_key: string,
  ref: string,
  customer_ref: string
}

export interface PersonalizedPath {
  [key: string]: any
}

export function getDynamicData(
  language: string,
  friendlyId:string,
  browserId: string
): Promise<DynamicData> {
 
  return callFlows({
    friendlyId: friendlyId,
    language: language,
    browserId: browserId
  }) as Promise<DynamicData>;
}

BoxeverApiServiceConfig.ts

const POINT_OF_SALE = process.env.CDP_POINT_OF_SALE;
const CURRENCY = 'USD';
const CDP_CLIENT_KEY = process.env.CDP_CLIENT_KEY || '';

const BoxeverApiServiceConfig = {
  channel: 'WEB',
  clientKey: CDP_CLIENT_KEY,
  pointOfSale: POINT_OF_SALE,
  currencyCode: CURRENCY
};

export default BoxeverApiServiceConfig;



Friday, April 1, 2022

Adding Coveo for Sitecore module to MCC

In previous blog post I shared the changes that I had to make to deploy a Next.js solution to Sitecore MCC and Vercel. I added JSS and SPE modules to my images and deployed them to my Azure MCC instance. In addition to JSS and SPE I had to add Coveo for Sitecore to my images which I am going to talk about in this blog post.

Coveo Documentation and Coveo for Sitecore Docker images

Coveo doesn’t have official support for running Coveo for Sitecore on containers, but they have provided sufficient information on how it can be done.

You can find high level instructions in Coveo documentation at https://docs.coveo.com/en/lb8f0136/coveo-for-sitecore-v5/install-coveo-for-sitecore-with-sitecore-in-docker

In this documentation you’ll find a reference to a fork that contains the Coveo for Sitecore assets as well as Coveo for Sitecore SXA images. I specifically was interested in Coveo for Sitecore assets for Sitecore 10.2. At the time this blog post was written there was 10.2 version of Docker images, hence I had to create my own fork with configurations for 10.2. (https://github.com/jcore/sitecore-docker-images)

The images that were created I pushed the project registry. I can’t share the registry link here, but you can use any registry that is available to you.

Solution Changes

Now I have a Coveo for Sitecore Docker for Sitecore 10.2 image that I can use in my solution. Coveo provided a fork that I used in my solution as well (https://docs.coveo.com/en/lb8f0136/coveo-for-sitecore-v5/install-coveo-for-sitecore-with-sitecore-in-docker#step-2-launch-sitecore-docker-instances-with-coveo-for-sitecore-pre-installed).  You can find at https://github.com/coveooss/sitecore-docker-examples/tree/develop/custom-images

In my solution I made the following changes:

.env

# Coveo env section

COVEO_INIT_IMAGE=[You registry url where the Docker image you created in previous step was deployed. Ex. [registry url]/custom-coveo5011101-assets:10.2.0-20220328.1]

COVEO_VERSION=[Coveo version without dots. Ex. 5011101]

COVEO_API_KEY=[API Key created in Sitecore (see Coveo API Keys section of this blog post.)]

COVEO_SEARCH_API_KEY=[API Key created in Sitecore (see Coveo API Keys section of this blog post.)]

COVEO_ORG_ID=[Coveo organization ID that you can find in Coveo Cloud Portal]

COVEO_USER=[Sitecore user that Coveo for Sitecore module will use.]

COVEO_PASSWORD=[Sitecore Coveo account password]

COVEO_FARMNAME=[Coveo farm name]

 

Adding COVEO_API_KEY in Coveo

Create an API key in Coveo Cloud portal. Copy the guid and add it to your .env file for the COVEO_API_KEY variable.

Go back to the Coveo Cloud portal and double click on the key that you just added and make sure that Privileges are set to be as follows:




If privileges are not set correctly, the activation script that runs during the application pipeline execution will fail.

 

Adding COVEO_SEARCH_API_KEY in Coveo

Add the second key to Coveo API keys for the search. Add the guid for the key to your .env file.

Go back to the Coveo Cloud portal and double click on the key that you just added and make sure that Privileges are set to be as follows:





Copying COVEO_ORG_ID from Coveo

Go to Organization section in the navigation on the left and click on Settings. In the modal window select Organization tab and click on Information menu item. Here you’ll find the Organization ID value. Copy and paste it into your .env file.



Other variables

Create a user in Sitecore for Coveo to use the same way you would do it when you install Coveo for Sitecore module in a non-container instance.

Coveo farm name can be anything or even empty for local environment.

 

Updating docker-compose.yml

  coveo-init:

    isolation: ${ISOLATION}

    image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-coveo-init:${COVEO_VERSION}-${SITECORE_VERSION}

    environment:

      COVEO_API_KEY: ${COVEO_API_KEY}

      COVEO_SEARCH_API_KEY: ${COVEO_SEARCH_API_KEY}

      COVEO_ORG_ID: ${COVEO_ORG_ID}

      COVEO_USER: sitecore\${COVEO_USER}

      COVEO_PASSWORD: ${COVEO_PASSWORD}

      COVEO_HOST: http://cm

      SITECORE_ADMIN_USER: sitecore\${SITECORE_ADMIN_USER}

      SITECORE_ADMIN_PASSWORD: ${SITECORE_ADMIN_PASSWORD}

      COVEO_FARMNAME: ${COVEO_FARMNAME}

    depends_on:

      cm:

        condition: service_healthy

      mssql:

        condition: service_healthy      

 

Updating docker-compose.override.yml

  coveo-init:

    image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-coveo-init:${VERSION:-latest}

    build:

      context: ./docker/build/coveo-init

      args:

        BASE_IMAGE: mcr.microsoft.com/powershell:nanoserver-1809

 

Adding coveo-init image

Take the code for this image from Coveo fork. I didn’t make any changes to these files.



CM, CD, and MSSQL-Init image changes

In docker-compose.override.yml add the following line to cm, cd and mssql-init images under args:

COVEO_ASSET: ${COVEO_INIT_IMAGE}

 

Like so:

  mssql-init:

    image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xp1-mssql-init:${VERSION:-latest}

    build:

      context: ./docker/build/mssql-init

      args:

        BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xp1-mssql-init:${SITECORE_VERSION}

        SPE_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-spe-assets:${SPE_VERSION}

        HEADLESS_SERVICES_IMAGE: ${HEADLESS_SERVICES_IMAGE}

        COVEO_ASSET: ${COVEO_INIT_IMAGE} # No buildable 10.2 version tags yet.

 

Now try running “docker-compose build” command. If it succeeds, you can run docker-compose up -d to make sure the Coveo for Sitecore module is showing up in CM and is also activated.

If everything works as expected, you can try deploying the new images to MCC.

 

Deploying to MCC

Push new cm, cd, mssql-init and coveo-init images to your Azure Registry.

In application repository for MCC the following changes had to be made:

1.       Update the image references for cm, cd, and mssql-init in .

2.       Add a new image reference called coveoinit.



3.       Under roles/sitecore-xp/templates add a new file called coveo-init.yaml with the following content:

.  ---

5.  apiVersion: batch/v1

6.  kind: Job

7.  metadata:

8.    name: coveo-init

9.  spec:

10.   template:

11.     spec:

12.       nodeSelector:

13.         kubernetes.io/os: windows

14.       imagePullSecrets:

15.         - name: sitecore-docker-registry

16.       containers:

17.       - name: coveo-init

18.         image: "{{ docker_images.sitecore.coveoinit }}"

19.         env:

20.         - name: COVEO_API_KEY

21.           valueFrom:

22.             secretKeyRef:

23.               name: sitecore-coveo-api-key

24.               key: sitecore-coveo-api-key.txt

25.         - name: COVEO_SEARCH_API_KEY

26.           valueFrom:

27.             secretKeyRef:

28.               name: sitecore-coveo-search-api-key

29.               key: sitecore-coveo-search-api-key.txt

30.         - name: COVEO_ORG_ID

31.           valueFrom:

32.             secretKeyRef:

33.               name: sitecore-coveo-org-id

34.               key: sitecore-coveo-org-id.txt

35.         - name: COVEO_USER

36.           valueFrom:

37.             secretKeyRef:

38.               name: sitecore-coveo-user

39.               key: sitecore-coveo-user.txt

40.         - name: COVEO_PASSWORD

41.           valueFrom:

42.             secretKeyRef:

43.               name: sitecore-coveo-password

44.               key: sitecore-coveo-password.txt

45.         - name: COVEO_HOST

46.           valueFrom:

47.             secretKeyRef:

48.               key: cm      

49.               name: sitecore-hostname

50.         - name: COVEO_FARMNAME

51.           valueFrom:

52.             secretKeyRef:

53.               key: sitecore-coveo-farmname.txt      

54.               name: sitecore-coveo-farmname              

55.         - name: SITECORE_ADMIN_USER

56.           valueFrom:

57.             secretKeyRef:

58.               name: sitecore-admin-domain-username

59.               key: sitecore-admin-domain-username.txt  

60.         - name: SITECORE_ADMIN_PASSWORD

61.           valueFrom:

62.             secretKeyRef:

63.               name: sitecore-admin

64.               key: sitecore-adminpassword.txt            

65.       restartPolicy: Never

66.   backoffLimit: 5

67.  

4. Add secret keys for all new variable in the file above.

5. In the same folder open secrets.yaml file and add the following section right before “parameters”.

  - data:

    - key: sitecore-coveo-api-key.txt

      objectName: sitecore-coveo-api-key

    secretName: sitecore-coveo-api-key

    type: Opaque    

  - data:

    - key: sitecore-coveo-search-api-key.txt

      objectName: sitecore-coveo-search-api-key

    secretName: sitecore-coveo-search-api-key

    type: Opaque    

  - data:

    - key: sitecore-coveo-org-id.txt

      objectName: sitecore-coveo-org-id

    secretName: sitecore-coveo-org-id

    type: Opaque        

  - data:

    - key: sitecore-coveo-user.txt

      objectName: sitecore-coveo-user

    secretName: sitecore-coveo-user

    type: Opaque    

  - data:

    - key: sitecore-coveo-password.txt

      objectName: sitecore-coveo-password

    secretName: sitecore-coveo-password

    type: Opaque  

  - data:

    - key: sitecore-coveo-farmname.txt

      objectName: sitecore-coveo-farmname

    secretName: sitecore-coveo-farmname

    type: Opaque      

  - data:

    - key: sitecore-admin-domain-username.txt

      objectName: sitecore-admin-domain-username

    secretName: sitecore-admin-domain-username

    type: Opaque  

At the end of objects array in parameters section of the same file add the following section at the very end of the string:

        - |

          objectName: sitecore-coveo-api-key  

          objectType: secret

        - |

          objectName: sitecore-coveo-search-api-key  

          objectType: secret            

        - |

          objectName: sitecore-coveo-org-id  

          objectType: secret        

        - |

          objectName: sitecore-coveo-user  

          objectType: secret        

        - |

          objectName: sitecore-coveo-password  

          objectType: secret  

        - |

          objectName: sitecore-coveo-farmname  

          objectType: secret        

        - |

          objectName: sitecore-admin-domain-username  

          objectType: secret    

6. At the end of main.yaml file in roles/sitecore-xp/tasks folder add the following:

 

- name: Execute post-init containers

  include_tasks: post-init.yaml

  when: hadr_resources.isExecuteInitContainer|bool==true      

7. In the same folder add a new post-init.yaml file with the following content:

---

- name: 'Wait - cm running'

  k8s_info:

    kind: Pod

    label_selectors:

      - app = cm

    namespace: "{{ solution_id }}"

    wait: true

    wait_sleep: 30

    wait_timeout: 180

    wait_condition:

      type: Ready

      status: "True"

 

- name: Execute coveo-init job

  k8s:

    apply: true

    namespace: "{{ solution_id }}"

    state: present

    definition: "{{ lookup('template', 'coveo-init.yaml') }}"

 

- name: 'Wait - coveo-init job'

  k8s_info:

    kind: Job

    name: coveo-init

    namespace: "{{ solution_id }}"

  register: coveo_init_result

  until: (coveo_init_result.resources[0].status.conditions[0].type | default('')) == 'Complete'

  retries: 30

  delay: 30

 

- name: Get all coveo-init completed pods

  k8s_info:

    kind: Pod

    namespace: "{{ solution_id }}"

    label_selectors:

      - job-name = coveo-init

  no_log: true

  register: coveo_pod_list

 

- name: Remove coveo-init job's pods

  k8s:

    kind: Pod

    name: "{{ item.metadata.name }}"

    namespace: "{{ solution_id }}"

    state: absent

  no_log: true

  with_items: "{{ coveo_pod_list.resources }}"

 

- name: Remove coveo-init job

  k8s:

    kind: Job

    name: coveo-init

    namespace: "{{ solution_id }}"

    state: absent

  no_log: true  

This is the Ansible instructions for running of coveo-init job. The job will spin off a new pod as well. To make sure that Coveo activation happens every time you do a deployment, the Ansible script deletes the pod and the job.

 

Check in the changes, create a pull request and merge the branch with your changes to the master branch. That should trigger the application pipeline execution.

If everything runs without error, you see Coveo for Sitecore module installed in your CM instance, and it is enabled, congratulations! If you are having issues, you can remove the deletion of the job and the pod temporarily to check if you see errors there. Often secret keys are not configured correctly, that would cause the job to fail.